@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.
Files changed (114) hide show
  1. package/README.md +59 -0
  2. package/index.ts +146 -0
  3. package/package.json +56 -0
  4. package/src/cli/app.ts +75 -0
  5. package/src/cli/config-command.ts +140 -0
  6. package/src/cli/config.ts +91 -0
  7. package/src/cli/derive.ts +205 -0
  8. package/src/cli/doc/annotation.ts +36 -0
  9. package/src/cli/doc/filter.ts +69 -0
  10. package/src/cli/doc/index.ts +9 -0
  11. package/src/cli/doc/post.ts +155 -0
  12. package/src/cli/doc/primitives.ts +25 -0
  13. package/src/cli/doc/render.ts +18 -0
  14. package/src/cli/doc/table.ts +114 -0
  15. package/src/cli/doc/thread.ts +46 -0
  16. package/src/cli/doc/tree.ts +126 -0
  17. package/src/cli/errors.ts +59 -0
  18. package/src/cli/exit-codes.ts +52 -0
  19. package/src/cli/feed.ts +177 -0
  20. package/src/cli/filter-dsl.ts +1411 -0
  21. package/src/cli/filter-errors.ts +208 -0
  22. package/src/cli/filter-help.ts +70 -0
  23. package/src/cli/filter-input.ts +54 -0
  24. package/src/cli/filter.ts +435 -0
  25. package/src/cli/graph.ts +472 -0
  26. package/src/cli/help.ts +14 -0
  27. package/src/cli/interval.ts +35 -0
  28. package/src/cli/jetstream.ts +173 -0
  29. package/src/cli/layers.ts +180 -0
  30. package/src/cli/logging.ts +136 -0
  31. package/src/cli/output-format.ts +26 -0
  32. package/src/cli/output.ts +82 -0
  33. package/src/cli/parse.ts +80 -0
  34. package/src/cli/post.ts +193 -0
  35. package/src/cli/preferences.ts +11 -0
  36. package/src/cli/query-fields.ts +247 -0
  37. package/src/cli/query.ts +415 -0
  38. package/src/cli/range.ts +44 -0
  39. package/src/cli/search.ts +465 -0
  40. package/src/cli/shared-options.ts +169 -0
  41. package/src/cli/shared.ts +20 -0
  42. package/src/cli/store-errors.ts +80 -0
  43. package/src/cli/store-tree.ts +392 -0
  44. package/src/cli/store.ts +395 -0
  45. package/src/cli/sync-factory.ts +107 -0
  46. package/src/cli/sync.ts +366 -0
  47. package/src/cli/view-thread.ts +196 -0
  48. package/src/cli/view.ts +47 -0
  49. package/src/cli/watch.ts +344 -0
  50. package/src/db/migrations/store-catalog/001_init.ts +14 -0
  51. package/src/db/migrations/store-index/001_init.ts +34 -0
  52. package/src/db/migrations/store-index/002_event_log.ts +24 -0
  53. package/src/db/migrations/store-index/003_fts_and_derived.ts +52 -0
  54. package/src/db/migrations/store-index/004_query_indexes.ts +9 -0
  55. package/src/db/migrations/store-index/005_post_lang.ts +15 -0
  56. package/src/db/migrations/store-index/006_has_embed.ts +10 -0
  57. package/src/db/migrations/store-index/007_event_seq_and_checkpoints.ts +68 -0
  58. package/src/domain/bsky.ts +467 -0
  59. package/src/domain/config.ts +11 -0
  60. package/src/domain/credentials.ts +6 -0
  61. package/src/domain/defaults.ts +8 -0
  62. package/src/domain/derivation.ts +55 -0
  63. package/src/domain/errors.ts +71 -0
  64. package/src/domain/events.ts +55 -0
  65. package/src/domain/extract.ts +64 -0
  66. package/src/domain/filter-describe.ts +551 -0
  67. package/src/domain/filter-explain.ts +9 -0
  68. package/src/domain/filter.ts +797 -0
  69. package/src/domain/format.ts +91 -0
  70. package/src/domain/index.ts +13 -0
  71. package/src/domain/indexes.ts +17 -0
  72. package/src/domain/policies.ts +16 -0
  73. package/src/domain/post.ts +88 -0
  74. package/src/domain/primitives.ts +50 -0
  75. package/src/domain/raw.ts +140 -0
  76. package/src/domain/store.ts +103 -0
  77. package/src/domain/sync.ts +211 -0
  78. package/src/domain/text-width.ts +56 -0
  79. package/src/services/app-config.ts +278 -0
  80. package/src/services/bsky-client.ts +2113 -0
  81. package/src/services/credential-store.ts +408 -0
  82. package/src/services/derivation-engine.ts +502 -0
  83. package/src/services/derivation-settings.ts +61 -0
  84. package/src/services/derivation-validator.ts +68 -0
  85. package/src/services/filter-compiler.ts +269 -0
  86. package/src/services/filter-library.ts +371 -0
  87. package/src/services/filter-runtime.ts +821 -0
  88. package/src/services/filter-settings.ts +30 -0
  89. package/src/services/identity-resolver.ts +563 -0
  90. package/src/services/jetstream-sync.ts +636 -0
  91. package/src/services/lineage-store.ts +89 -0
  92. package/src/services/link-validator.ts +244 -0
  93. package/src/services/output-manager.ts +274 -0
  94. package/src/services/post-parser.ts +62 -0
  95. package/src/services/profile-resolver.ts +223 -0
  96. package/src/services/resource-monitor.ts +106 -0
  97. package/src/services/shared.ts +69 -0
  98. package/src/services/store-cleaner.ts +43 -0
  99. package/src/services/store-commit.ts +168 -0
  100. package/src/services/store-db.ts +248 -0
  101. package/src/services/store-event-log.ts +285 -0
  102. package/src/services/store-index-sql.ts +289 -0
  103. package/src/services/store-index.ts +1152 -0
  104. package/src/services/store-keys.ts +4 -0
  105. package/src/services/store-manager.ts +358 -0
  106. package/src/services/store-stats.ts +522 -0
  107. package/src/services/store-writer.ts +200 -0
  108. package/src/services/sync-checkpoint-store.ts +169 -0
  109. package/src/services/sync-engine.ts +547 -0
  110. package/src/services/sync-reporter.ts +16 -0
  111. package/src/services/sync-settings.ts +72 -0
  112. package/src/services/trending-topics.ts +226 -0
  113. package/src/services/view-checkpoint-store.ts +238 -0
  114. package/src/typeclass/chunk.ts +84 -0
package/README.md ADDED
@@ -0,0 +1,59 @@
1
+ # skygent-bsky
2
+
3
+ To install dependencies:
4
+
5
+ ```bash
6
+ bun install
7
+ ```
8
+
9
+ Create a local `.env` from `.env.example` and set credentials:
10
+
11
+ ```bash
12
+ cp .env.example .env
13
+ ```
14
+
15
+ To run:
16
+
17
+ ```bash
18
+ bun run index.ts
19
+ ```
20
+
21
+ ## Quickstart
22
+
23
+ ```bash
24
+ # List stores (compact JSON for agents)
25
+ bun run index.ts store list --compact
26
+
27
+ # Sync timeline into a store
28
+ bun run index.ts sync timeline --store my-store --quiet
29
+
30
+ # Query recent posts as a table
31
+ bun run index.ts query my-store --limit 10 --format table
32
+
33
+ # Stream Jetstream posts
34
+ bun run index.ts watch jetstream --store my-store --quiet
35
+ ```
36
+
37
+ Tips:
38
+ - Add `--compact` to reduce JSON output size.
39
+ - Add `--quiet` to suppress progress logs during sync/watch commands.
40
+
41
+ ## Documentation
42
+
43
+ - docs/README.md
44
+ - docs/getting-started.md
45
+ - docs/cli.md
46
+ - docs/filters.md
47
+ - docs/configuration.md
48
+ - docs/credentials.md
49
+ - docs/stores.md
50
+ - docs/outputs.md
51
+
52
+ ## Security notes
53
+
54
+ - Credentials are loaded via Effect `Redacted` config and are not logged.
55
+ - Encrypted credential storage uses `SKYGENT_CREDENTIALS_KEY` (AES-GCM).
56
+ - Avoid putting passwords in `config.json`; use env or the credential store instead.
57
+ - Resource warnings can be configured via `SKYGENT_RESOURCE_*` env vars.
58
+
59
+ This project was created using `bun init` in bun v1.3.4. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
package/index.ts ADDED
@@ -0,0 +1,146 @@
1
+ #!/usr/bin/env bun
2
+ import { Command, HelpDoc, ValidationError } from "@effect/cli";
3
+ import { BunContext, BunRuntime } from "@effect/platform-bun";
4
+ import { Effect, Layer } from "effect";
5
+ import { app } from "./src/cli/app.js";
6
+ import pkg from "./package.json" with { type: "json" };
7
+ import {
8
+ type AgentErrorPayload,
9
+ CliInputError,
10
+ CliJsonError,
11
+ parseAgentErrorPayload
12
+ } from "./src/cli/errors.js";
13
+ import { logErrorEvent } from "./src/cli/logging.js";
14
+ import { CliOutput } from "./src/cli/output.js";
15
+ import { exitCodeFor, exitCodeFromExit } from "./src/cli/exit-codes.js";
16
+ import { BskyError, StoreNotFound } from "./src/domain/errors.js";
17
+
18
+ const cli = Command.run(app, {
19
+ name: "skygent",
20
+ version: pkg.version
21
+ });
22
+
23
+ const stripAnsi = (value: string) =>
24
+ value.replace(/\u001b\[[0-9;]*m/g, "");
25
+
26
+ const formatValidationError = (error: ValidationError.ValidationError) => {
27
+ const text = HelpDoc.toAnsiText(error.error).trimEnd();
28
+ return stripAnsi(text.length > 0 ? text : "Invalid command input. Use --help for usage.");
29
+ };
30
+
31
+ const getAgentPayload = (error: unknown): AgentErrorPayload | undefined => {
32
+ if (error instanceof CliJsonError || error instanceof CliInputError) {
33
+ return parseAgentErrorPayload(error.message);
34
+ }
35
+ return undefined;
36
+ };
37
+
38
+ const formatError = (error: unknown, agentPayload?: AgentErrorPayload) => {
39
+ if (agentPayload) {
40
+ return agentPayload.message;
41
+ }
42
+ if (ValidationError.isValidationError(error)) {
43
+ return formatValidationError(error);
44
+ }
45
+ if (error instanceof CliJsonError) {
46
+ return error.message;
47
+ }
48
+ if (error instanceof CliInputError) {
49
+ return error.message;
50
+ }
51
+ if (error instanceof StoreNotFound) {
52
+ return `Store \"${error.name}\" does not exist.`;
53
+ }
54
+ if (error instanceof Error) {
55
+ return error.message;
56
+ }
57
+ if (
58
+ typeof error === "object" &&
59
+ error !== null &&
60
+ "message" in error &&
61
+ typeof (error as { readonly message?: unknown }).message === "string"
62
+ ) {
63
+ return (error as { readonly message: string }).message;
64
+ }
65
+ if (typeof error === "object" && error !== null && "_tag" in error) {
66
+ return JSON.stringify(error);
67
+ }
68
+ return String(error);
69
+ };
70
+
71
+ const errorType = (error: unknown, agentPayload?: AgentErrorPayload): string => {
72
+ if (agentPayload) return agentPayload.error;
73
+ if (ValidationError.isValidationError(error)) return "ValidationError";
74
+ if (error instanceof Error) return error.name;
75
+ if (typeof error === "object" && error !== null && "_tag" in error) {
76
+ return String((error as { readonly _tag?: unknown })._tag ?? "UnknownError");
77
+ }
78
+ return "UnknownError";
79
+ };
80
+
81
+ const errorDetails = (
82
+ error: unknown,
83
+ agentPayload?: AgentErrorPayload
84
+ ): Record<string, unknown> | undefined => {
85
+ if (agentPayload) {
86
+ return { error: agentPayload };
87
+ }
88
+ if (error instanceof StoreNotFound) {
89
+ return { error: { _tag: "StoreNotFound", name: error.name } };
90
+ }
91
+ if (error instanceof BskyError) {
92
+ return {
93
+ error: {
94
+ _tag: "BskyError",
95
+ ...(error.operation ? { operation: error.operation } : {}),
96
+ ...(typeof error.status === "number" ? { status: error.status } : {})
97
+ }
98
+ };
99
+ }
100
+ if (error instanceof CliJsonError || error instanceof CliInputError) {
101
+ return undefined;
102
+ }
103
+ if (typeof error === "object" && error !== null && "_tag" in error) {
104
+ return { error };
105
+ }
106
+ return undefined;
107
+ };
108
+
109
+ const errorSuggestion = (
110
+ error: unknown,
111
+ agentPayload?: AgentErrorPayload
112
+ ): string | undefined => {
113
+ if (agentPayload?.fix) return agentPayload.fix;
114
+ if (error instanceof StoreNotFound) {
115
+ return `Run: skygent store create ${error.name}`;
116
+ }
117
+ return undefined;
118
+ };
119
+
120
+ const program = cli(process.argv).pipe(
121
+ Effect.tapError((error) => {
122
+ if (ValidationError.isValidationError(error)) {
123
+ return Effect.void;
124
+ }
125
+ const agentPayload = getAgentPayload(error);
126
+ return logErrorEvent(formatError(error, agentPayload), {
127
+ code: exitCodeFor(error),
128
+ type: errorType(error, agentPayload),
129
+ suggestion: errorSuggestion(error, agentPayload),
130
+ ...errorDetails(error, agentPayload)
131
+ });
132
+ }),
133
+ Effect.provide(Layer.mergeAll(BunContext.layer, CliOutput.layer))
134
+ );
135
+
136
+ BunRuntime.runMain({
137
+ disableErrorReporting: true,
138
+ disablePrettyLogger: true,
139
+ teardown: (exit, onExit) => {
140
+ const code = exitCodeFromExit(exit);
141
+ onExit(code);
142
+ if (typeof process !== "undefined") {
143
+ process.exit(code);
144
+ }
145
+ }
146
+ })(program);
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@mepuka/skygent",
3
+ "version": "0.2.0",
4
+ "description": "Composable Bluesky data filtering and monitoring CLI built with Effect",
5
+ "module": "index.ts",
6
+ "type": "module",
7
+ "bin": {
8
+ "skygent": "./index.ts"
9
+ },
10
+ "files": [
11
+ "index.ts",
12
+ "src/",
13
+ "package.json"
14
+ ],
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "https://github.com/mepuka/skygent-bsky.git"
18
+ },
19
+ "scripts": {
20
+ "prepare": "effect-language-service patch",
21
+ "build": "tsc --build",
22
+ "build:clean": "rm -rf dist .tsbuildinfo && bun run build",
23
+ "build:binary": "bun build --compile index.ts --outfile skygent",
24
+ "build:binary:linux-x64": "bun build --compile --target=bun-linux-x64 index.ts --outfile skygent-linux-x64",
25
+ "build:binary:linux-arm64": "bun build --compile --target=bun-linux-arm64 index.ts --outfile skygent-linux-arm64",
26
+ "build:binary:darwin-x64": "bun build --compile --target=bun-darwin-x64 index.ts --outfile skygent-darwin-x64",
27
+ "build:binary:darwin-arm64": "bun build --compile --target=bun-darwin-arm64 index.ts --outfile skygent-darwin-arm64",
28
+ "test": "bun test",
29
+ "test:watch": "bun test --watch",
30
+ "typecheck": "tsc --noEmit",
31
+ "changeset": "changeset",
32
+ "version": "changeset version",
33
+ "release": "bun run build && changeset publish"
34
+ },
35
+ "devDependencies": {
36
+ "@changesets/cli": "^2.29.8",
37
+ "@effect/language-service": "^0.72.0",
38
+ "@types/bun": "latest"
39
+ },
40
+ "peerDependencies": {
41
+ "typescript": "^5"
42
+ },
43
+ "dependencies": {
44
+ "@atproto/api": "^0.18.17",
45
+ "@effect/cli": "^0.73.1",
46
+ "@effect/platform": "^0.94.2",
47
+ "@effect/platform-bun": "^0.87.1",
48
+ "@effect/printer": "^0.47.0",
49
+ "@effect/printer-ansi": "^0.47.0",
50
+ "@effect/sql": "^0.49.0",
51
+ "@effect/sql-sqlite-bun": "^0.50.0",
52
+ "@effect/typeclass": "^0.38.0",
53
+ "effect": "^3.19.15",
54
+ "effect-jetstream": "^1.0.4"
55
+ }
56
+ }
package/src/cli/app.ts ADDED
@@ -0,0 +1,75 @@
1
+ import { Command } from "@effect/cli";
2
+ import { Layer, Option } from "effect";
3
+ import { CliLive } from "./layers.js";
4
+ import { ConfigOverrides } from "../services/app-config.js";
5
+ import { CredentialsOverrides } from "../services/credential-store.js";
6
+ import { DerivationSettingsOverrides } from "../services/derivation-settings.js";
7
+ import { SyncSettingsOverrides } from "../services/sync-settings.js";
8
+ import { CliPreferences } from "./preferences.js";
9
+ import { storeCommand } from "./store.js";
10
+ import { syncCommand } from "./sync.js";
11
+ import { queryCommand } from "./query.js";
12
+ import { watchCommand } from "./watch.js";
13
+ import { deriveCommand } from "./derive.js";
14
+ import { viewCommand } from "./view.js";
15
+ import { filterCommand } from "./filter.js";
16
+ import { searchCommand } from "./search.js";
17
+ import { graphCommand } from "./graph.js";
18
+ import { feedCommand } from "./feed.js";
19
+ import { postCommand } from "./post.js";
20
+ import { configCommand } from "./config-command.js";
21
+ import {
22
+ configOptions,
23
+ toConfigOverrides,
24
+ toCredentialsOverrides,
25
+ toSyncSettingsOverrides
26
+ } from "./config.js";
27
+ import { withExamples } from "./help.js";
28
+
29
+ export const app = Command.make("skygent", configOptions).pipe(
30
+ Command.withSubcommands([
31
+ configCommand,
32
+ storeCommand,
33
+ syncCommand,
34
+ queryCommand,
35
+ watchCommand,
36
+ deriveCommand,
37
+ viewCommand,
38
+ filterCommand,
39
+ searchCommand,
40
+ graphCommand,
41
+ feedCommand,
42
+ postCommand
43
+ ]),
44
+ Command.provide((config) =>
45
+ Layer.mergeAll(
46
+ CliLive.pipe(
47
+ Layer.provide(
48
+ Layer.mergeAll(
49
+ Layer.succeed(ConfigOverrides, toConfigOverrides(config)),
50
+ Layer.succeed(CredentialsOverrides, toCredentialsOverrides(config)),
51
+ Layer.succeed(SyncSettingsOverrides, toSyncSettingsOverrides(config)),
52
+ DerivationSettingsOverrides.layer
53
+ )
54
+ )
55
+ ),
56
+ Layer.succeed(
57
+ CliPreferences,
58
+ Option.match(config.logFormat, {
59
+ onNone: () => ({ compact: config.compact }),
60
+ onSome: (logFormat) => ({ compact: config.compact, logFormat })
61
+ })
62
+ )
63
+ )
64
+ ),
65
+ Command.withDescription(
66
+ withExamples(
67
+ "Skygent CLI for Bluesky monitoring",
68
+ [
69
+ "skygent store list --compact",
70
+ "skygent sync timeline --store my-store --quiet"
71
+ ],
72
+ ["Tip: add --compact for shorter JSON output."]
73
+ )
74
+ )
75
+ );
@@ -0,0 +1,140 @@
1
+ import { Command } from "@effect/cli";
2
+ import { FileSystem, Path } from "@effect/platform";
3
+ import { Options } from "@effect/cli";
4
+ import { Clock, Effect, Option, Stream } from "effect";
5
+ import { withExamples } from "./help.js";
6
+ import { writeJson, writeText } from "./output.js";
7
+ import { renderTableLegacy } from "./doc/table.js";
8
+ import { AppConfigService } from "../services/app-config.js";
9
+ import { CredentialStore } from "../services/credential-store.js";
10
+ import { BskyClient } from "../services/bsky-client.js";
11
+ import { jsonTableFormats, resolveOutputFormat } from "./output-format.js";
12
+
13
+ type CheckStatus = "ok" | "warn" | "error";
14
+
15
+ type CheckResult = {
16
+ readonly name: string;
17
+ readonly status: CheckStatus;
18
+ readonly message?: string;
19
+ };
20
+
21
+ const checkOk = (name: string, message?: string): CheckResult =>
22
+ message ? { name, status: "ok", message } : { name, status: "ok" };
23
+
24
+ const checkWarn = (name: string, message: string): CheckResult => ({
25
+ name,
26
+ status: "warn",
27
+ message
28
+ });
29
+
30
+ const checkError = (name: string, message: string): CheckResult => ({
31
+ name,
32
+ status: "error",
33
+ message
34
+ });
35
+
36
+ const checkFormatOption = Options.choice("format", jsonTableFormats).pipe(
37
+ Options.withDescription("Output format (default: json)"),
38
+ Options.optional
39
+ );
40
+
41
+ const configCheckCommand = Command.make("check", { format: checkFormatOption }, ({ format }) =>
42
+ Effect.gen(function* () {
43
+ const results: Array<CheckResult> = [];
44
+
45
+ const fs = yield* FileSystem.FileSystem;
46
+ const path = yield* Path.Path;
47
+ const config = yield* AppConfigService;
48
+ const credentials = yield* CredentialStore;
49
+ const bsky = yield* BskyClient;
50
+ // Store root writable
51
+ const rootCheck = yield* Effect.gen(function* () {
52
+ yield* fs.makeDirectory(config.storeRoot, { recursive: true });
53
+ const now = yield* Clock.currentTimeMillis;
54
+ const probePath = path.join(
55
+ config.storeRoot,
56
+ `.skygent-check-${now}`
57
+ );
58
+ yield* fs.writeFileString(probePath, "ok");
59
+ yield* fs.remove(probePath);
60
+ return checkOk("store-root", "Store root is writable.");
61
+ }).pipe(
62
+ Effect.catchAll((error) =>
63
+ Effect.succeed(
64
+ checkError(
65
+ "store-root",
66
+ error instanceof Error ? error.message : String(error)
67
+ )
68
+ )
69
+ )
70
+ );
71
+ results.push(rootCheck);
72
+
73
+ // Credential file + key
74
+ const credentialCheck = yield* credentials.get().pipe(
75
+ Effect.match({
76
+ onFailure: (error) =>
77
+ checkError("credentials", error.message ?? "Failed to load credentials"),
78
+ onSuccess: (value) =>
79
+ Option.isSome(value)
80
+ ? checkOk("credentials", "Credentials loaded.")
81
+ : checkWarn("credentials", "No credentials configured.")
82
+ })
83
+ );
84
+ results.push(credentialCheck);
85
+
86
+ // Bluesky auth
87
+ if (credentialCheck.status === "ok") {
88
+ const bskyCheck = yield* bsky
89
+ .getTimeline({ limit: 1 })
90
+ .pipe(
91
+ Stream.take(1),
92
+ Stream.runCollect,
93
+ Effect.as(checkOk("bluesky", "Bluesky login succeeded.")),
94
+ Effect.catchAll((error) =>
95
+ Effect.succeed(
96
+ checkError(
97
+ "bluesky",
98
+ error instanceof Error ? error.message : String(error)
99
+ )
100
+ )
101
+ )
102
+ );
103
+ results.push(bskyCheck);
104
+ } else {
105
+ results.push(
106
+ checkWarn("bluesky", "Skipped Bluesky login (credentials missing).")
107
+ );
108
+ }
109
+
110
+ const ok = results.every((result) => result.status !== "error");
111
+ const outputFormat = resolveOutputFormat(
112
+ format,
113
+ config.outputFormat,
114
+ jsonTableFormats,
115
+ "json"
116
+ );
117
+
118
+ if (outputFormat === "table") {
119
+ const rows = results.map((r) => [
120
+ r.status === "ok" ? "✓" : r.status === "warn" ? "⚠" : "✗",
121
+ r.name,
122
+ r.message || r.status
123
+ ]);
124
+ const table = renderTableLegacy(["STATUS", "CHECK", "DETAILS"], rows);
125
+ yield* writeText(`${ok ? "✓ Config valid" : "✗ Config has errors"}\n\n${table}`);
126
+ return;
127
+ }
128
+
129
+ yield* writeJson({ ok, checks: results });
130
+ })
131
+ ).pipe(
132
+ Command.withDescription("Run health checks (store root, credentials, Bluesky auth)")
133
+ );
134
+
135
+ export const configCommand = Command.make("config", {}).pipe(
136
+ Command.withSubcommands([configCheckCommand]),
137
+ Command.withDescription(
138
+ withExamples("Configuration helpers", ["skygent config check"])
139
+ )
140
+ );
@@ -0,0 +1,91 @@
1
+ import { Options } from "@effect/cli";
2
+ import { Option, Redacted } from "effect";
3
+ import { pickDefined } from "../services/shared.js";
4
+ import { OutputFormat } from "../domain/config.js";
5
+ import { AppConfig } from "../domain/config.js";
6
+ import type { LogFormat } from "./logging.js";
7
+ import type { SyncSettingsValue } from "../services/sync-settings.js";
8
+ import type { CredentialsOverridesValue } from "../services/credential-store.js";
9
+
10
+
11
+ export const configOptions = {
12
+ service: Options.text("service").pipe(
13
+ Options.optional,
14
+ Options.withDescription("Override the Bluesky service URL")
15
+ ),
16
+ storeRoot: Options.text("store-root").pipe(
17
+ Options.optional,
18
+ Options.withDescription("Override the root storage directory")
19
+ ),
20
+ outputFormat: Options.choice("output-format", [
21
+ "json",
22
+ "ndjson",
23
+ "markdown",
24
+ "table"
25
+ ]).pipe(Options.optional, Options.withDescription("Default output format")),
26
+ identifier: Options.text("identifier").pipe(
27
+ Options.optional,
28
+ Options.withDescription("Override Bluesky identifier")
29
+ ),
30
+ password: Options.redacted("password").pipe(
31
+ Options.optional,
32
+ Options.withDescription("Override Bluesky password (redacted)")
33
+ ),
34
+ compact: Options.boolean("compact").pipe(
35
+ Options.withDescription("Reduce JSON output verbosity for agent consumption")
36
+ ),
37
+ logFormat: Options.choice("log-format", ["json", "human"]).pipe(
38
+ Options.optional,
39
+ Options.withDescription("Override log format (json or human)")
40
+ ),
41
+ syncConcurrency: Options.integer("sync-concurrency").pipe(
42
+ Options.optional,
43
+ Options.withDescription("Concurrent sync preparation workers (default: 5)")
44
+ ),
45
+ checkpointEvery: Options.integer("checkpoint-every").pipe(
46
+ Options.optional,
47
+ Options.withDescription("Checkpoint every N processed posts (default: 100)")
48
+ ),
49
+ checkpointIntervalMs: Options.integer("checkpoint-interval-ms").pipe(
50
+ Options.optional,
51
+ Options.withDescription("Checkpoint interval in milliseconds (default: 5000)")
52
+ )
53
+ };
54
+
55
+ export type ConfigOptions = {
56
+ readonly service: Option.Option<string>;
57
+ readonly storeRoot: Option.Option<string>;
58
+ readonly outputFormat: Option.Option<OutputFormat>;
59
+ readonly identifier: Option.Option<string>;
60
+ readonly password: Option.Option<Redacted.Redacted<string>>;
61
+ readonly compact: boolean;
62
+ readonly logFormat: Option.Option<LogFormat>;
63
+ readonly syncConcurrency: Option.Option<number>;
64
+ readonly checkpointEvery: Option.Option<number>;
65
+ readonly checkpointIntervalMs: Option.Option<number>;
66
+ };
67
+
68
+ export const toConfigOverrides = (options: ConfigOptions): Partial<AppConfig> =>
69
+ pickDefined({
70
+ service: Option.getOrUndefined(options.service),
71
+ storeRoot: Option.getOrUndefined(options.storeRoot),
72
+ outputFormat: Option.getOrUndefined(options.outputFormat),
73
+ identifier: Option.getOrUndefined(options.identifier)
74
+ }) as Partial<AppConfig>;
75
+
76
+ export const toCredentialsOverrides = (
77
+ options: ConfigOptions
78
+ ): CredentialsOverridesValue =>
79
+ pickDefined({
80
+ identifier: Option.getOrUndefined(options.identifier),
81
+ password: Option.getOrUndefined(options.password)
82
+ }) as CredentialsOverridesValue;
83
+
84
+ export const toSyncSettingsOverrides = (
85
+ options: ConfigOptions
86
+ ): Partial<SyncSettingsValue> =>
87
+ pickDefined({
88
+ concurrency: Option.getOrUndefined(options.syncConcurrency),
89
+ checkpointEvery: Option.getOrUndefined(options.checkpointEvery),
90
+ checkpointIntervalMs: Option.getOrUndefined(options.checkpointIntervalMs)
91
+ }) as Partial<SyncSettingsValue>;