@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/README.md
CHANGED
|
@@ -1,59 +1,297 @@
|
|
|
1
|
-
# skygent
|
|
1
|
+
# @mepuka/skygent
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Composable Bluesky data filtering and monitoring CLI built with [Effect](https://effect.website).
|
|
4
|
+
|
|
5
|
+
Sync posts from timelines, feeds, lists, authors, and the real-time Jetstream firehose into local SQLite stores. Query, filter, derive, and export data with a powerful filter DSL and multiple output formats.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
### npm / bun
|
|
4
10
|
|
|
5
11
|
```bash
|
|
12
|
+
bun add -g @mepuka/skygent
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
### From source
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
git clone https://github.com/mepuka/skygent-bsky.git
|
|
19
|
+
cd skygent-bsky
|
|
6
20
|
bun install
|
|
21
|
+
bun run index.ts --help
|
|
7
22
|
```
|
|
8
23
|
|
|
9
|
-
|
|
24
|
+
### Standalone binary
|
|
25
|
+
|
|
26
|
+
Download a prebuilt binary from [GitHub Releases](https://github.com/mepuka/skygent-bsky/releases), or build locally:
|
|
10
27
|
|
|
11
28
|
```bash
|
|
12
|
-
|
|
29
|
+
bun run build:binary
|
|
30
|
+
./skygent --help
|
|
13
31
|
```
|
|
14
32
|
|
|
15
|
-
|
|
33
|
+
Cross-platform targets: `linux-x64`, `linux-arm64`, `darwin-x64`, `darwin-arm64`.
|
|
34
|
+
|
|
35
|
+
## Authentication
|
|
36
|
+
|
|
37
|
+
Skygent needs a Bluesky handle and [app password](https://bsky.app/settings/app-passwords). Credentials are resolved in this order:
|
|
38
|
+
|
|
39
|
+
1. CLI flags: `--identifier` and `--password`
|
|
40
|
+
2. Environment variables: `SKYGENT_IDENTIFIER` and `SKYGENT_PASSWORD`
|
|
41
|
+
3. Encrypted credential file (`~/.skygent/credentials.json`, requires `SKYGENT_CREDENTIALS_KEY`)
|
|
42
|
+
|
|
43
|
+
The simplest setup:
|
|
16
44
|
|
|
17
45
|
```bash
|
|
18
|
-
|
|
46
|
+
cp .env.example .env
|
|
47
|
+
# Edit .env with your handle and app password
|
|
19
48
|
```
|
|
20
49
|
|
|
50
|
+
Bun loads `.env` automatically.
|
|
51
|
+
|
|
21
52
|
## Quickstart
|
|
22
53
|
|
|
23
54
|
```bash
|
|
24
|
-
#
|
|
25
|
-
|
|
55
|
+
# Create a store
|
|
56
|
+
skygent store create my-store
|
|
57
|
+
|
|
58
|
+
# Sync your timeline
|
|
59
|
+
skygent sync timeline --store my-store
|
|
26
60
|
|
|
27
|
-
#
|
|
28
|
-
|
|
61
|
+
# Query recent posts
|
|
62
|
+
skygent query my-store --limit 10 --format table
|
|
29
63
|
|
|
30
|
-
#
|
|
31
|
-
|
|
64
|
+
# Stream live posts from Jetstream
|
|
65
|
+
skygent watch jetstream --store my-store
|
|
32
66
|
|
|
33
|
-
#
|
|
34
|
-
|
|
67
|
+
# Derive a filtered store
|
|
68
|
+
skygent derive my-store ai-posts --filter 'hashtag:#ai OR hashtag:#ml'
|
|
69
|
+
|
|
70
|
+
# Search posts locally
|
|
71
|
+
skygent search posts "effect typescript" --store my-store
|
|
35
72
|
```
|
|
36
73
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
74
|
+
## Commands
|
|
75
|
+
|
|
76
|
+
### `store` -- Manage local stores
|
|
77
|
+
|
|
78
|
+
| Subcommand | Description |
|
|
79
|
+
|---|---|
|
|
80
|
+
| `store create <name>` | Create a new store |
|
|
81
|
+
| `store list` | List all stores |
|
|
82
|
+
| `store show <name>` | Show store config and metadata |
|
|
83
|
+
| `store rename <from> <to>` | Rename a store |
|
|
84
|
+
| `store delete <name> --force` | Delete a store |
|
|
85
|
+
| `store stats <name>` | Show store statistics |
|
|
86
|
+
| `store summary` | Summarize all stores |
|
|
87
|
+
| `store tree` | Visualize store lineage |
|
|
88
|
+
| `store materialize <name>` | Materialize filter outputs to disk |
|
|
89
|
+
|
|
90
|
+
### `sync` -- One-time data sync
|
|
91
|
+
|
|
92
|
+
| Subcommand | Description |
|
|
93
|
+
|---|---|
|
|
94
|
+
| `sync timeline` | Sync your timeline |
|
|
95
|
+
| `sync feed <uri>` | Sync a feed generator |
|
|
96
|
+
| `sync list <uri>` | Sync a list feed |
|
|
97
|
+
| `sync author <actor>` | Sync posts from an author |
|
|
98
|
+
| `sync thread <uri>` | Sync a thread (parents + replies) |
|
|
99
|
+
| `sync notifications` | Sync notifications |
|
|
100
|
+
| `sync jetstream` | Sync from Jetstream firehose |
|
|
101
|
+
|
|
102
|
+
All sync commands accept `--store`, `--filter`, `--quiet`, and `--refresh`.
|
|
103
|
+
|
|
104
|
+
### `watch` -- Continuous sync
|
|
105
|
+
|
|
106
|
+
Same subcommands as `sync`, with continuous polling. Supports `--interval` (default: 30s).
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
skygent watch timeline --store my-store --interval "5 minutes"
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### `query` -- Query stored posts
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
skygent query my-store --limit 25 --format table
|
|
116
|
+
skygent query my-store --filter 'hashtag:#ai' --sort desc --format json
|
|
117
|
+
skygent query my-store --range 2024-01-01T00:00:00Z..2024-01-31T00:00:00Z
|
|
118
|
+
skygent query my-store --fields @minimal --newest-first
|
|
119
|
+
skygent query store-a,store-b --format ndjson
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
**Formats:** `json`, `ndjson`, `table`, `markdown`, `compact`, `card`, `thread`
|
|
123
|
+
|
|
124
|
+
**Field presets:** `@minimal`, `@social`, `@full`, or comma-separated field names with dot notation.
|
|
125
|
+
|
|
126
|
+
Multi-store queries accept comma-separated store lists or repeated store arguments and include store names in output by default.
|
|
127
|
+
|
|
128
|
+
### `derive` -- Create derived stores
|
|
129
|
+
|
|
130
|
+
Apply a filter to a source store to produce a new filtered store:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
skygent derive source-store target-store --filter 'hashtag:#ai'
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
**Modes:**
|
|
137
|
+
- `event-time` (default) -- Pure filters only, replayable
|
|
138
|
+
- `derive-time` -- Allows effectful filters (Trending, HasValidLinks)
|
|
139
|
+
|
|
140
|
+
### `filter` -- Filter management and testing
|
|
141
|
+
|
|
142
|
+
Tip: run `skygent filter help` for a compact list of predicates and aliases.
|
|
143
|
+
|
|
144
|
+
| Subcommand | Description |
|
|
145
|
+
|---|---|
|
|
146
|
+
| `filter create <name>` | Save a named filter |
|
|
147
|
+
| `filter list` | List saved filters |
|
|
148
|
+
| `filter show <name>` | Show a saved filter |
|
|
149
|
+
| `filter delete <name>` | Delete a saved filter |
|
|
150
|
+
| `filter help` | Show filter DSL and JSON help |
|
|
151
|
+
| `filter validate` | Validate a filter expression |
|
|
152
|
+
| `filter test` | Test a filter against a post |
|
|
153
|
+
| `filter explain` | Explain why a post matches |
|
|
154
|
+
| `filter benchmark` | Benchmark filter performance |
|
|
155
|
+
| `filter describe` | Describe a filter in plain text |
|
|
156
|
+
|
|
157
|
+
### `search` -- Search content
|
|
158
|
+
|
|
159
|
+
| Subcommand | Description |
|
|
160
|
+
|---|---|
|
|
161
|
+
| `search posts <query>` | Search posts locally or `--network` |
|
|
162
|
+
| `search handles <query>` | Search Bluesky profiles |
|
|
163
|
+
| `search feeds <query>` | Search feed generators |
|
|
164
|
+
|
|
165
|
+
### `graph` -- Social graph
|
|
166
|
+
|
|
167
|
+
| Subcommand | Description |
|
|
168
|
+
|---|---|
|
|
169
|
+
| `graph followers <actor>` | List followers |
|
|
170
|
+
| `graph follows <actor>` | List follows |
|
|
171
|
+
| `graph known-followers <actor>` | Mutual followers |
|
|
172
|
+
| `graph relationships <actor>` | Relationship status |
|
|
173
|
+
| `graph lists <actor>` | Lists created by actor |
|
|
174
|
+
| `graph list <uri>` | View a list's members |
|
|
175
|
+
| `graph blocks` | Your blocked accounts |
|
|
176
|
+
| `graph mutes` | Your muted accounts |
|
|
177
|
+
|
|
178
|
+
### `feed` -- Feed generators
|
|
179
|
+
|
|
180
|
+
| Subcommand | Description |
|
|
181
|
+
|---|---|
|
|
182
|
+
| `feed show <uri>` | Show feed details |
|
|
183
|
+
| `feed batch <uri>...` | Fetch multiple feeds |
|
|
184
|
+
| `feed by <actor>` | List feeds by an actor |
|
|
185
|
+
|
|
186
|
+
### `post` -- Post engagement
|
|
187
|
+
|
|
188
|
+
| Subcommand | Description |
|
|
189
|
+
|---|---|
|
|
190
|
+
| `post likes <uri>` | Who liked a post |
|
|
191
|
+
| `post reposted-by <uri>` | Who reposted |
|
|
192
|
+
| `post quotes <uri>` | Quote posts |
|
|
193
|
+
|
|
194
|
+
### `view` -- Inspect threads and derivations
|
|
195
|
+
|
|
196
|
+
| Subcommand | Description |
|
|
197
|
+
|---|---|
|
|
198
|
+
| `view thread <uri>` | Display a thread |
|
|
199
|
+
| `view status <view> <source>` | Check if a derived view is stale |
|
|
200
|
+
|
|
201
|
+
### `config` -- Configuration
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
skygent config check # Run health checks
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## Filter DSL
|
|
208
|
+
|
|
209
|
+
Filters are passed via `--filter` (DSL string) or `--filter-json` (JSON AST).
|
|
210
|
+
|
|
211
|
+
### Primitives
|
|
212
|
+
|
|
213
|
+
| Filter | Example |
|
|
214
|
+
|---|---|
|
|
215
|
+
| `hashtag:#tag` | Match posts with hashtag |
|
|
216
|
+
| `author:handle.bsky.social` | Match posts by author |
|
|
217
|
+
| `contains:"text"` | Text search (case-insensitive by default) |
|
|
218
|
+
| `regex:/pattern/i` | Regex match |
|
|
219
|
+
| `language:en,es` | Match languages |
|
|
220
|
+
| `date:<start>..<end>` | Date range (ISO 8601) |
|
|
221
|
+
| `engagement:minLikes=100` | Engagement thresholds |
|
|
222
|
+
| `is:reply` | Post type (`reply`, `quote`, `repost`, `original`) |
|
|
223
|
+
| `has:images` | Media presence (`images`, `video`, `links`, `media`, `embed`) |
|
|
224
|
+
| `@saved-name` | Reference a saved filter |
|
|
225
|
+
|
|
226
|
+
### Aliases
|
|
227
|
+
|
|
228
|
+
`from:` = `author:`, `tag:` = `hashtag:`, `text:` = `contains:`, `lang:` = `language:`
|
|
229
|
+
|
|
230
|
+
### List filters
|
|
231
|
+
|
|
232
|
+
`authorin:alice,bob,charlie` and `hashtagin:#ai,#ml,#dl`
|
|
233
|
+
|
|
234
|
+
### Boolean operators
|
|
235
|
+
|
|
236
|
+
```
|
|
237
|
+
hashtag:#ai AND author:user.bsky.social
|
|
238
|
+
hashtag:#ai OR hashtag:#ml
|
|
239
|
+
NOT hashtag:#spam
|
|
240
|
+
(hashtag:#ai OR hashtag:#ml) AND engagement:minLikes=10
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Operators: `AND` / `&&`, `OR` / `||`, `NOT` / `!`, parentheses for grouping.
|
|
244
|
+
|
|
245
|
+
## Configuration
|
|
246
|
+
|
|
247
|
+
### Environment variables
|
|
248
|
+
|
|
249
|
+
| Variable | Default | Description |
|
|
250
|
+
|---|---|---|
|
|
251
|
+
| `SKYGENT_IDENTIFIER` | -- | Bluesky handle or DID |
|
|
252
|
+
| `SKYGENT_PASSWORD` | -- | App password |
|
|
253
|
+
| `SKYGENT_CREDENTIALS_KEY` | -- | Master key for encrypted credential storage |
|
|
254
|
+
| `SKYGENT_SERVICE` | `https://bsky.social` | Bluesky service URL |
|
|
255
|
+
| `SKYGENT_STORE_ROOT` | `~/.skygent` | Root storage directory |
|
|
256
|
+
| `SKYGENT_OUTPUT_FORMAT` | `ndjson` | Default output format |
|
|
257
|
+
| `SKYGENT_BSKY_RATE_LIMIT` | `250 millis` | Min delay between API calls |
|
|
258
|
+
| `SKYGENT_BSKY_RETRY_MAX` | `5` | Max retry attempts |
|
|
259
|
+
| `SKYGENT_SYNC_CONCURRENCY` | `5` | Concurrent sync workers |
|
|
260
|
+
|
|
261
|
+
### Global flags
|
|
262
|
+
|
|
263
|
+
- `--full` -- Use verbose JSON output (compact is the default)
|
|
264
|
+
- `--quiet` -- Suppress progress output
|
|
265
|
+
- `--log-format json|human` -- Control log format
|
|
266
|
+
|
|
267
|
+
## Architecture
|
|
268
|
+
|
|
269
|
+
Skygent is built entirely on Effect with a layered service architecture:
|
|
270
|
+
|
|
271
|
+
- **Domain** (`src/domain/`) -- Data models for posts, stores, filters, events, and derivations using Effect Schema
|
|
272
|
+
- **Services** (`src/services/`) -- Business logic: Bluesky API client, SQLite store, sync engine, filter runtime, derivation engine
|
|
273
|
+
- **CLI** (`src/cli/`) -- Command definitions, output formatting, error handling
|
|
274
|
+
|
|
275
|
+
Stores are local SQLite databases with an append-only event log. Derivations track lineage between stores and support incremental processing with checkpoints.
|
|
276
|
+
|
|
277
|
+
## Security
|
|
278
|
+
|
|
279
|
+
- Passwords are handled as `Redacted` values and never logged
|
|
280
|
+
- Encrypted credential storage uses AES-GCM with PBKDF2 (100,000 iterations)
|
|
281
|
+
- Avoid putting passwords in config files; use environment variables or the credential store
|
|
40
282
|
|
|
41
283
|
## Documentation
|
|
42
284
|
|
|
43
|
-
|
|
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
|
|
285
|
+
Detailed docs are in `docs/`:
|
|
51
286
|
|
|
52
|
-
|
|
287
|
+
- [Getting Started](docs/getting-started.md)
|
|
288
|
+
- [CLI Reference](docs/cli.md)
|
|
289
|
+
- [Filters](docs/filters.md)
|
|
290
|
+
- [Configuration](docs/configuration.md)
|
|
291
|
+
- [Credentials](docs/credentials.md)
|
|
292
|
+
- [Stores](docs/stores.md)
|
|
293
|
+
- [Output Formats](docs/outputs.md)
|
|
53
294
|
|
|
54
|
-
|
|
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.
|
|
295
|
+
## License
|
|
58
296
|
|
|
59
|
-
|
|
297
|
+
MIT
|
package/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { Command, HelpDoc, ValidationError } from "@effect/cli";
|
|
3
3
|
import { BunContext, BunRuntime } from "@effect/platform-bun";
|
|
4
|
-
import { Effect, Layer } from "effect";
|
|
4
|
+
import { Clock, Effect, Layer } from "effect";
|
|
5
5
|
import { app } from "./src/cli/app.js";
|
|
6
6
|
import pkg from "./package.json" with { type: "json" };
|
|
7
7
|
import {
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
import { logErrorEvent } from "./src/cli/logging.js";
|
|
14
14
|
import { CliOutput } from "./src/cli/output.js";
|
|
15
15
|
import { exitCodeFor, exitCodeFromExit } from "./src/cli/exit-codes.js";
|
|
16
|
-
import { BskyError, StoreNotFound } from "./src/domain/errors.js";
|
|
16
|
+
import { BskyError, StoreAlreadyExists, StoreNotFound } from "./src/domain/errors.js";
|
|
17
17
|
|
|
18
18
|
const cli = Command.run(app, {
|
|
19
19
|
name: "skygent",
|
|
@@ -51,6 +51,9 @@ const formatError = (error: unknown, agentPayload?: AgentErrorPayload) => {
|
|
|
51
51
|
if (error instanceof StoreNotFound) {
|
|
52
52
|
return `Store \"${error.name}\" does not exist.`;
|
|
53
53
|
}
|
|
54
|
+
if (error instanceof StoreAlreadyExists) {
|
|
55
|
+
return `Store \"${error.name}\" already exists.`;
|
|
56
|
+
}
|
|
54
57
|
if (error instanceof Error) {
|
|
55
58
|
return error.message;
|
|
56
59
|
}
|
|
@@ -88,6 +91,9 @@ const errorDetails = (
|
|
|
88
91
|
if (error instanceof StoreNotFound) {
|
|
89
92
|
return { error: { _tag: "StoreNotFound", name: error.name } };
|
|
90
93
|
}
|
|
94
|
+
if (error instanceof StoreAlreadyExists) {
|
|
95
|
+
return { error: { _tag: "StoreAlreadyExists", name: error.name } };
|
|
96
|
+
}
|
|
91
97
|
if (error instanceof BskyError) {
|
|
92
98
|
return {
|
|
93
99
|
error: {
|
|
@@ -114,6 +120,9 @@ const errorSuggestion = (
|
|
|
114
120
|
if (error instanceof StoreNotFound) {
|
|
115
121
|
return `Run: skygent store create ${error.name}`;
|
|
116
122
|
}
|
|
123
|
+
if (error instanceof StoreAlreadyExists) {
|
|
124
|
+
return "Run: skygent store list";
|
|
125
|
+
}
|
|
117
126
|
return undefined;
|
|
118
127
|
};
|
|
119
128
|
|
|
@@ -130,7 +139,13 @@ const program = cli(process.argv).pipe(
|
|
|
130
139
|
...errorDetails(error, agentPayload)
|
|
131
140
|
});
|
|
132
141
|
}),
|
|
133
|
-
Effect.provide(
|
|
142
|
+
Effect.provide(
|
|
143
|
+
Layer.mergeAll(
|
|
144
|
+
BunContext.layer,
|
|
145
|
+
CliOutput.layer,
|
|
146
|
+
Layer.succeed(Clock.Clock, Clock.make())
|
|
147
|
+
)
|
|
148
|
+
)
|
|
134
149
|
);
|
|
135
150
|
|
|
136
151
|
BunRuntime.runMain({
|
package/package.json
CHANGED
package/src/cli/app.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { searchCommand } from "./search.js";
|
|
|
17
17
|
import { graphCommand } from "./graph.js";
|
|
18
18
|
import { feedCommand } from "./feed.js";
|
|
19
19
|
import { postCommand } from "./post.js";
|
|
20
|
+
import { pipeCommand } from "./pipe.js";
|
|
20
21
|
import { configCommand } from "./config-command.js";
|
|
21
22
|
import {
|
|
22
23
|
configOptions,
|
|
@@ -39,7 +40,8 @@ export const app = Command.make("skygent", configOptions).pipe(
|
|
|
39
40
|
searchCommand,
|
|
40
41
|
graphCommand,
|
|
41
42
|
feedCommand,
|
|
42
|
-
postCommand
|
|
43
|
+
postCommand,
|
|
44
|
+
pipeCommand
|
|
43
45
|
]),
|
|
44
46
|
Command.provide((config) =>
|
|
45
47
|
Layer.mergeAll(
|
|
@@ -69,7 +71,7 @@ export const app = Command.make("skygent", configOptions).pipe(
|
|
|
69
71
|
"skygent store list --compact",
|
|
70
72
|
"skygent sync timeline --store my-store --quiet"
|
|
71
73
|
],
|
|
72
|
-
["Tip:
|
|
74
|
+
["Tip: compact output is the default; use --full for verbose JSON."]
|
|
73
75
|
)
|
|
74
76
|
)
|
|
75
77
|
);
|
package/src/cli/config.ts
CHANGED
|
@@ -8,6 +8,13 @@ import type { SyncSettingsValue } from "../services/sync-settings.js";
|
|
|
8
8
|
import type { CredentialsOverridesValue } from "../services/credential-store.js";
|
|
9
9
|
|
|
10
10
|
|
|
11
|
+
const compactOption = Options.boolean("full", {
|
|
12
|
+
negationNames: ["compact"]
|
|
13
|
+
}).pipe(
|
|
14
|
+
Options.withDescription("Use full JSON output (disables compact default)"),
|
|
15
|
+
Options.map((full) => !full)
|
|
16
|
+
);
|
|
17
|
+
|
|
11
18
|
export const configOptions = {
|
|
12
19
|
service: Options.text("service").pipe(
|
|
13
20
|
Options.optional,
|
|
@@ -31,9 +38,7 @@ export const configOptions = {
|
|
|
31
38
|
Options.optional,
|
|
32
39
|
Options.withDescription("Override Bluesky password (redacted)")
|
|
33
40
|
),
|
|
34
|
-
compact:
|
|
35
|
-
Options.withDescription("Reduce JSON output verbosity for agent consumption")
|
|
36
|
-
),
|
|
41
|
+
compact: compactOption,
|
|
37
42
|
logFormat: Options.choice("log-format", ["json", "human"]).pipe(
|
|
38
43
|
Options.optional,
|
|
39
44
|
Options.withDescription("Override log format (json or human)")
|
|
@@ -42,6 +47,14 @@ export const configOptions = {
|
|
|
42
47
|
Options.optional,
|
|
43
48
|
Options.withDescription("Concurrent sync preparation workers (default: 5)")
|
|
44
49
|
),
|
|
50
|
+
syncBatchSize: Options.integer("sync-batch-size").pipe(
|
|
51
|
+
Options.optional,
|
|
52
|
+
Options.withDescription("Batch size for sync store writes (default: 100)")
|
|
53
|
+
),
|
|
54
|
+
syncPageLimit: Options.integer("sync-page-limit").pipe(
|
|
55
|
+
Options.optional,
|
|
56
|
+
Options.withDescription("Page size for sync fetches (default: 100)")
|
|
57
|
+
),
|
|
45
58
|
checkpointEvery: Options.integer("checkpoint-every").pipe(
|
|
46
59
|
Options.optional,
|
|
47
60
|
Options.withDescription("Checkpoint every N processed posts (default: 100)")
|
|
@@ -61,6 +74,8 @@ export type ConfigOptions = {
|
|
|
61
74
|
readonly compact: boolean;
|
|
62
75
|
readonly logFormat: Option.Option<LogFormat>;
|
|
63
76
|
readonly syncConcurrency: Option.Option<number>;
|
|
77
|
+
readonly syncBatchSize: Option.Option<number>;
|
|
78
|
+
readonly syncPageLimit: Option.Option<number>;
|
|
64
79
|
readonly checkpointEvery: Option.Option<number>;
|
|
65
80
|
readonly checkpointIntervalMs: Option.Option<number>;
|
|
66
81
|
};
|
|
@@ -86,6 +101,8 @@ export const toSyncSettingsOverrides = (
|
|
|
86
101
|
): Partial<SyncSettingsValue> =>
|
|
87
102
|
pickDefined({
|
|
88
103
|
concurrency: Option.getOrUndefined(options.syncConcurrency),
|
|
104
|
+
batchSize: Option.getOrUndefined(options.syncBatchSize),
|
|
105
|
+
pageLimit: Option.getOrUndefined(options.syncPageLimit),
|
|
89
106
|
checkpointEvery: Option.getOrUndefined(options.checkpointEvery),
|
|
90
107
|
checkpointIntervalMs: Option.getOrUndefined(options.checkpointIntervalMs)
|
|
91
108
|
}) as Partial<SyncSettingsValue>;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { renderTableLegacy } from "./table.js";
|
|
2
|
+
import type { FeedGeneratorView, ProfileView } from "../../domain/bsky.js";
|
|
3
|
+
|
|
4
|
+
export const renderProfileTable = (
|
|
5
|
+
actors: ReadonlyArray<ProfileView>,
|
|
6
|
+
cursor: string | undefined
|
|
7
|
+
) => {
|
|
8
|
+
const rows = actors.map((actor) => [
|
|
9
|
+
actor.handle,
|
|
10
|
+
actor.displayName ?? "",
|
|
11
|
+
actor.did
|
|
12
|
+
]);
|
|
13
|
+
const table = renderTableLegacy(["HANDLE", "DISPLAY NAME", "DID"], rows);
|
|
14
|
+
return cursor ? `${table}\n\nCursor: ${cursor}` : table;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const renderFeedTable = (
|
|
18
|
+
feeds: ReadonlyArray<FeedGeneratorView>,
|
|
19
|
+
cursor: string | undefined
|
|
20
|
+
) => {
|
|
21
|
+
const rows = feeds.map((feed) => [
|
|
22
|
+
feed.displayName,
|
|
23
|
+
feed.creator.handle,
|
|
24
|
+
feed.uri,
|
|
25
|
+
typeof feed.likeCount === "number" ? String(feed.likeCount) : ""
|
|
26
|
+
]);
|
|
27
|
+
const table = renderTableLegacy(["NAME", "CREATOR", "URI", "LIKES"], rows);
|
|
28
|
+
return cursor ? `${table}\n\nCursor: ${cursor}` : table;
|
|
29
|
+
};
|
package/src/cli/doc/thread.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { Annotation } from "./annotation.js";
|
|
|
3
3
|
import { renderTree } from "./tree.js";
|
|
4
4
|
import { renderPostCompact, renderPostCardLines } from "./post.js";
|
|
5
5
|
import type { Post } from "../../domain/post.js";
|
|
6
|
+
import { PostOrder } from "../../domain/order.js";
|
|
6
7
|
|
|
7
8
|
export const renderThread = (
|
|
8
9
|
posts: ReadonlyArray<Post>,
|
|
@@ -23,10 +24,7 @@ export const renderThread = (
|
|
|
23
24
|
}
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
const sortPosts = (arr: Post[]) =>
|
|
27
|
-
arr.sort((a, b) =>
|
|
28
|
-
a.createdAt.getTime() - b.createdAt.getTime() || a.uri.localeCompare(b.uri)
|
|
29
|
-
);
|
|
27
|
+
const sortPosts = (arr: Post[]) => arr.sort(PostOrder);
|
|
30
28
|
|
|
31
29
|
sortPosts(roots);
|
|
32
30
|
for (const children of childMap.values()) sortPosts(children);
|
package/src/cli/exit-codes.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
FilterEvalError,
|
|
9
9
|
FilterLibraryError,
|
|
10
10
|
FilterNotFound,
|
|
11
|
+
StoreAlreadyExists,
|
|
11
12
|
StoreIoError,
|
|
12
13
|
StoreIndexError,
|
|
13
14
|
StoreNotFound
|
|
@@ -21,6 +22,7 @@ export const exitCodeFor = (error: unknown): number => {
|
|
|
21
22
|
if (error instanceof CliInputError) return 2;
|
|
22
23
|
if (error instanceof ConfigError) return 2;
|
|
23
24
|
if (error instanceof StoreNotFound) return 3;
|
|
25
|
+
if (error instanceof StoreAlreadyExists) return 2;
|
|
24
26
|
if (error instanceof FilterNotFound) return 2;
|
|
25
27
|
if (error instanceof FilterLibraryError) return 2;
|
|
26
28
|
if (error instanceof StoreIoError || error instanceof StoreIndexError) return 7;
|