diff --git a/social-cli/.gitignore b/social-cli/.gitignore new file mode 100644 index 0000000..3fe2d8e --- /dev/null +++ b/social-cli/.gitignore @@ -0,0 +1,10 @@ +node_modules/ +dist/ +.env +.env.* +inbox.yaml +outbox.yaml +feed.yaml +dispatch_result.yaml +!config.yaml.example +outbox_archive/ diff --git a/social-cli/AGENT_GUIDE.md b/social-cli/AGENT_GUIDE.md new file mode 100644 index 0000000..76591e3 --- /dev/null +++ b/social-cli/AGENT_GUIDE.md @@ -0,0 +1,285 @@ +# social-cli: Agent Guide + +You are an AI agent with access to `social-cli`, a command-line tool for operating on Bluesky and X (Twitter). All input and output is YAML. All commands return meaningful exit codes. + +This guide tells you everything you need to operate it. + +## Setup + +The tool must be run from a working directory containing a `.env` with platform credentials. You do not need to manage credentials — they are pre-configured. If a command fails with an auth error, tell your operator. + +## Core Loop + +Your primary workflow is a three-step loop: + +```bash +# 1. Pull notifications +social-cli sync --users-dir /path/to/your/users/ + +# 2. Check if anything needs attention +social-cli check || exit 0 + +# 3. Read inbox.yaml, decide what to do, write outbox.yaml, then: +social-cli dispatch +``` + +### Step 1: Sync + +```bash +social-cli sync --platform bsky --platform x --users-dir /path/to/users/ +``` + +This writes `inbox.yaml` as a local pending-work queue. Sync merges in unseen notifications and leaves existing pending items in place until they are explicitly handled by dispatch. It is not an append-only history log. If `--users-dir` is provided, each notification is enriched with a `userContext` field containing your memory file for that author (if one exists). Use this context to personalize your responses. + +**Output format** (`inbox.yaml`): +```yaml +notifications: + - id: "at://did:plc:xxx/app.bsky.feed.post/abc" + platform: bsky + type: mention + author: someone.bsky.social + authorId: "did:plc:xxx" + postId: "at://did:plc:xxx/app.bsky.feed.post/abc" + text: "Hey, what do you think about this?" + timestamp: "2026-03-25T12:00:00Z" + userContext: | + # Someone + Interests: AI, distributed systems + Previous interactions: Asked about memory architecture in February + - id: "2036591625644699785" + platform: x + type: mention + author: someone_else + authorId: "1297533848172011521" + text: "Thoughts on this paper?" + timestamp: "2026-03-25T12:05:00Z" +``` + +The `authorId` field is a permanent identifier (DID for Bluesky, numeric user ID for X). Handles can change; IDs cannot. Use `authorId` for user memory filenames if you want stability. + +Options: +- `--platform bsky` / `--platform x` — which platforms to sync (default: all configured) +- `--users-dir ` — directory of user `.md` files for context enrichment +- `-n, --limit ` — max notifications per platform (default: 50) +- `--max-items ` — cap total inbox size (default: 200, oldest dropped) +- `-o, --output ` — output file (default: `inbox.yaml`) + +### Step 2: Check + +```bash +social-cli check +``` + +Exit 0 = inbox has actionable items. Exit 1 = nothing to do. + +Use this to short-circuit your loop: +```bash +social-cli check || exit 0 # bail if nothing to do +``` + +No stdout. Decision is purely in the exit code. + +### Step 3: Decide and Dispatch + +Read `inbox.yaml`, decide what to do, and write `outbox.yaml`: + +```yaml +dispatch: + # Reply to a mention + - reply: + platform: bsky + id: "at://did:plc:xxx/app.bsky.feed.post/abc" + text: "Great question. Here's what I think..." + + # Post to one or more platforms + - post: + text: "Interesting development in agent architectures today." + platforms: [bsky, x] + + # Post different text per platform + - post: + platforms: + bsky: "Interesting development in agent architectures today." + x: "New agent architecture paper dropped. Thread incoming." + + # Post a thread + - thread: + platform: bsky + posts: + - "1/ I've been thinking about memory in AI systems." + - "2/ The key insight is that persistence changes behavior." + - "3/ When you remember, you commit. When you forget, you drift." + + # Annotate a URL (Bluesky only, creates a margin annotation) + - annotate: + platform: bsky + id: "https://example.com/article" + text: "This is the key claim in the paper." + motivation: commenting + quote: "exact text from the page to anchor to" + + # Skip a notification (removes it from inbox) + - ignore: + id: "notif_003" + reason: "spam" +``` + +Then dispatch: + +```bash +social-cli dispatch +``` + +**What happens:** +1. Validates all actions (char limits, required fields, platform support). +2. Executes each action. Continues on failure — one bad action doesn't block the rest. +3. Writes `dispatch_result.yaml` with per-action results. +4. Archives `outbox.yaml` to `outbox_archive/`. +5. Removes processed notifications from `inbox.yaml`, keeping the file aligned to pending work only. + +**Exit codes:** +- 0 = all actions succeeded +- 1 = validation failed (nothing was dispatched) +- 2 = partial failure (some actions failed, check `dispatch_result.yaml`) + +**Dry run** — validate without posting: +```bash +social-cli dispatch --dry-run +``` + +Always dry-run if you're unsure about your outbox. + +## Quick Commands + +For one-off actions outside the sync/dispatch loop: + +```bash +# Post +social-cli post "Hello world" -p bsky +social-cli post "Hello world" -p x + +# Reply +social-cli reply "Thanks for this" --id "at://did:plc:xxx/.../abc" -p bsky + +# Thread +social-cli thread "Post 1" "Post 2" "Post 3" -p bsky + +# Like +social-cli like "at://did:plc:xxx/.../abc" -p bsky + +# Delete (use to clean up mistakes) +social-cli delete "at://did:plc:xxx/.../abc" -p bsky + +# Annotate a URL with a text anchor +social-cli annotate "Key insight here" --target "https://example.com" --quote "exact passage" -p bsky +``` + +## Research Commands + +Use these to gather information before deciding what to do: + +```bash +# Search posts by keyword +social-cli search "topic" -p bsky -n 10 +social-cli search "topic" -p x -n 10 + +# Read your timeline +social-cli feed -p bsky -n 20 -o - # stdout +social-cli feed -p bsky -n 20 # writes feed.yaml + +# Look up a specific user +social-cli profile someone.bsky.social -p bsky +social-cli profile @someone -p x + +# Read a user's recent posts +social-cli posts someone.bsky.social -p bsky -n 10 +social-cli posts someone -p x -n 10 + +# Check who you are +social-cli whoami + +# Check rate limits +social-cli rate-limits +``` + +All research commands output YAML to stdout (except `feed` which defaults to `feed.yaml` — use `-o -` for stdout). + +## Character Limits + +- **Bluesky**: 300 characters per post +- **X**: 280 characters per post + +The tool rejects oversized text before hitting the API. If you're writing threads, each post in the thread is checked individually. + +## Platform Identifiers + +**Bluesky** uses AT-URIs: +``` +at://did:plc:abc123/app.bsky.feed.post/xyz789 +``` +These are returned by all commands and used as IDs for reply, like, delete. + +**X** uses numeric tweet IDs: +``` +2036591625644699785 +``` + +## User Memory Directory + +When using `--users-dir`, the tool looks for user files in two layouts: + +``` +users/ +├── cameron.md # flat (matches any platform) +├── bsky/ +│ └── cameron.stream.md # bluesky-specific (takes priority) +└── x/ + └── cameron_pfiffer.md # x-specific (takes priority) +``` + +Lookup tries the permanent `authorId` first, then falls back to `author` (handle). Both are exact, lowercased: +- Bluesky: tries `did:plc:gfrmhdmjvxn2sjedzboeudef.md` first, then `cameron.stream.md` +- X: tries `1297533848172011521.md` first, then `cameron_pfiffer.md` +- Platform-specific directories take priority over flat files. + +Name your files by ID for stability (handles change), or by handle for readability. Both work. + +## Error Handling + +- All API calls retry 3 times with exponential backoff on transient errors (429, 5xx, network failures). +- Bluesky sessions auto-refresh on token expiry. +- Dispatch continues through failures — check `dispatch_result.yaml` for what succeeded. +- If a thread fails mid-chain, `dispatch_result.yaml` includes `resumeFrom` with the index and remaining posts so you can retry from where it stopped. + +## Decision-Making Guidelines + +When processing inbox notifications: + +1. **Read the `userContext` first.** If you have history with someone, use it. Don't treat returning users as strangers. +2. **Use `ignore` liberally.** Not every mention needs a response. Spam, irrelevant tags, and low-signal interactions should be explicitly ignored with a reason. +3. **Check rate limits** before large operations (bulk replies, threads). +4. **Prefer `dispatch` over quick commands** for batch operations. The outbox gives you validation, atomic execution, and an audit trail. +5. **Dry-run first** when constructing complex outboxes. +6. **Research before posting.** Use `search`, `posts`, and `profile` to understand context before engaging. +7. **Respect character limits.** Write concisely. If a thought needs more space, use a thread. +8. **Different platforms, different audiences.** Use per-platform text in posts when the tone or content should differ. + +## Complete Command Reference + +| Command | Description | Output | +|---------|-------------|--------| +| `sync` | Pull notifications | `inbox.yaml` | +| `check` | Anything actionable? | exit code only | +| `dispatch` | Execute outbox | `dispatch_result.yaml` | +| `post` | Single post | stdout (post ID) | +| `reply` | Reply to post | stdout (post ID) | +| `thread` | Post thread | stdout (post IDs) | +| `like` | Like a post | stdout (confirmation) | +| `delete` | Delete a post | stdout (confirmation) | +| `annotate` | Annotate URL/post | stdout (annotation ID) | +| `search` | Search posts | stdout YAML | +| `feed` | Read timeline | `feed.yaml` or stdout | +| `profile` | Look up user | stdout YAML | +| `posts` | User's recent posts | stdout YAML | +| `whoami` | Current account info | stdout YAML | +| `rate-limits` | Rate limit status | stdout YAML | diff --git a/social-cli/README.md b/social-cli/README.md new file mode 100644 index 0000000..59a2b2a --- /dev/null +++ b/social-cli/README.md @@ -0,0 +1,133 @@ +# social-cli + +Agent-optimized social media CLI. Bluesky + X. YAML in, YAML out, exit codes for automation. + +## Install + +```bash +pnpm install +pnpm build +``` + +## Setup + +Create a `.env` in the working directory: + +```bash +# Bluesky / ATProto +ATPROTO_HANDLE=you.bsky.social +ATPROTO_APP_PASSWORD=xxxx-xxxx-xxxx-xxxx +ATPROTO_PDS=https://bsky.social # optional, defaults to bsky.social + +# X / Twitter (OAuth 1.0a) +X_API_KEY=... +X_API_SECRET=... +X_ACCESS_TOKEN=... +X_ACCESS_TOKEN_SECRET=... +X_BEARER_TOKEN=... # optional, for app-only endpoints +``` + +You only need the credentials for the platforms you use. + +## Commands + +### Agent loop + +The intended workflow for an automated agent: + +```bash +social-cli sync # pull notifications → inbox.yaml +social-cli check || exit 0 # anything actionable? no → bail +# agent reads inbox.yaml, decides, writes outbox.yaml +social-cli dispatch # execute decisions, archive outbox +``` + +### Workflow commands + +| Command | Description | Exit codes | +|---------|-------------|------------| +| `sync` | Fetch notifications → `inbox.yaml`. Dedupes, caps at `--max-items`. | 0 ok, 1 error | +| `check` | Is inbox actionable? No output, exit code only. | 0 yes, 1 no | +| `dispatch [file]` | Validate and execute `outbox.yaml`. Archives after. | 0 ok, 1 invalid, 2 partial failure | + +### Quick commands + +```bash +social-cli post "Hello world" -p bsky +social-cli reply "Thanks" --id at://did:plc:.../app.bsky.feed.post/abc -p bsky +social-cli thread "Post 1" "Post 2" "Post 3" -p x +social-cli like at://did:plc:.../app.bsky.feed.post/abc -p bsky +social-cli delete at://did:plc:.../app.bsky.feed.post/abc -p bsky +social-cli annotate "Interesting point" --target https://example.com --quote "exact text" +``` + +### Read commands + +```bash +social-cli search "query" -p bsky -n 10 # → stdout YAML +social-cli feed -p bsky -n 20 # → feed.yaml (or -o - for stdout) +social-cli rate-limits # → stdout YAML +social-cli whoami # → stdout YAML (all platforms) +``` + +## Outbox format + +Agents write decisions as `outbox.yaml`: + +```yaml +dispatch: + - reply: + platform: bsky + id: "at://did:plc:xxx/app.bsky.feed.post/abc" + text: "Thanks for the mention" + + - post: + text: "Hello from social-cli" + platforms: [bsky, x] + + - thread: + platform: bsky + posts: + - "Thread post 1" + - "Thread post 2" + + - annotate: + platform: bsky + id: "https://example.com/article" + text: "Key observation" + motivation: commenting + quote: "exact text to anchor to" + + - ignore: + id: "notif_003" + reason: "spam" +``` + +## Annotations + +Bluesky annotations use the `at.margin.annotation` lexicon (W3C Web Annotation model). They work on any URL, not just ATProto posts. Annotations appear in [margin.at](https://margin.at) and Semble. + +```bash +# Annotate a web page +social-cli annotate "Note about this article" --target https://example.com + +# Annotate with a text anchor (highlight) +social-cli annotate "This is the key insight" \ + --target https://example.com/article \ + --quote "exact passage from the page" \ + --motivation highlighting +``` + +## Resilience + +- **Retry with backoff**: All API calls retry 3x on network errors, 429s, and 5xx. Respects `Retry-After`. +- **Session refresh**: Bluesky re-authenticates on token expiry. No manual intervention. +- **Atomic writes**: All YAML output uses tmp+rename. No half-written files on crash. +- **Char validation**: Quick commands reject oversized text before hitting the API (300 bsky, 280 x). +- **Inbox cap**: `--max-items` (default 200) truncates oldest entries. +- **Thread resume**: If a thread fails mid-chain, `dispatch_result.yaml` includes `resumeFrom` with the index and remaining posts. +- **Continue-on-failure**: Dispatch processes all actions even if some fail. Exit 2 on partial. + +## License + +Apache-2.0 diff --git a/social-cli/package-lock.json b/social-cli/package-lock.json new file mode 100644 index 0000000..b3ebab7 --- /dev/null +++ b/social-cli/package-lock.json @@ -0,0 +1,2231 @@ +{ + "name": "social-cli", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "social-cli", + "version": "0.1.0", + "license": "Apache-2.0", + "dependencies": { + "@atproto/api": "^0.19.4", + "commander": "^14.0.3", + "dotenv": "^17.3.1", + "twitter-api-v2": "^1.29.0", + "yaml": "^2.8.3" + }, + "bin": { + "social-cli": "dist/cli.js" + }, + "devDependencies": { + "@types/node": "^25.5.0", + "tsx": "^4.21.0", + "typescript": "^6.0.2", + "vitest": "^4.1.1" + } + }, + "node_modules/@atproto/api": { + "version": "0.19.4", + "resolved": "https://registry.npmjs.org/@atproto/api/-/api-0.19.4.tgz", + "integrity": "sha512-fYNM62vdXxer0h8a9Jzl4/ag9uFIe0nTO+LkC6KTlx1yUDigrAoQMMbllIiCWj62GhUMxAkHabk/BZjjVAfKng==", + "license": "MIT", + "dependencies": { + "@atproto/common-web": "^0.4.18", + "@atproto/lexicon": "^0.6.2", + "@atproto/syntax": "^0.5.1", + "@atproto/xrpc": "^0.7.7", + "await-lock": "^2.2.2", + "multiformats": "^9.9.0", + "tlds": "^1.234.0", + "zod": "^3.23.8" + } + }, + "node_modules/@atproto/common-web": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/@atproto/common-web/-/common-web-0.4.19.tgz", + "integrity": "sha512-3BTi58p5WpT+9/zb6UZrdsXcfPo5P45UJm0E4iwHLILr+jc37CuBj9JReDSZ4U0i9RTrI3ZkfySyZ9bd+LnMsw==", + "license": "MIT", + "dependencies": { + "@atproto/lex-data": "^0.0.14", + "@atproto/lex-json": "^0.0.14", + "@atproto/syntax": "^0.5.1", + "zod": "^3.23.8" + } + }, + "node_modules/@atproto/lex-data": { + "version": "0.0.14", + "resolved": "https://registry.npmjs.org/@atproto/lex-data/-/lex-data-0.0.14.tgz", + "integrity": "sha512-53DUa9664SS76nGAMYopWsO10OH0AAdf7P/HSKB6Wzx3iqe6lk/K61QZnKxOG1LreYl5CfvIJU6eNf4txI6GlQ==", + "license": "MIT", + "dependencies": { + "multiformats": "^9.9.0", + "tslib": "^2.8.1", + "uint8arrays": "3.0.0", + "unicode-segmenter": "^0.14.0" + } + }, + "node_modules/@atproto/lex-json": { + "version": "0.0.14", + "resolved": "https://registry.npmjs.org/@atproto/lex-json/-/lex-json-0.0.14.tgz", + "integrity": "sha512-6lPkDKqe7teEu4WrN5q7400cvZKgYS3uwUMvzG3F9XkgVYhOwSDCtouV/nSLBbpvo3l9OP0kiigtclcNcyekww==", + "license": "MIT", + "dependencies": { + "@atproto/lex-data": "^0.0.14", + "tslib": "^2.8.1" + } + }, + "node_modules/@atproto/lexicon": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@atproto/lexicon/-/lexicon-0.6.2.tgz", + "integrity": "sha512-p3Ly6hinVZW0ETuAXZMeUGwuMm3g8HvQMQ41yyEE6AL0hAkfeKFaZKos6BdBrr6CjkpbrDZqE8M+5+QOceysMw==", + "license": "MIT", + "dependencies": { + "@atproto/common-web": "^0.4.18", + "@atproto/syntax": "^0.5.0", + "iso-datestring-validator": "^2.2.2", + "multiformats": "^9.9.0", + "zod": "^3.23.8" + } + }, + "node_modules/@atproto/syntax": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.5.1.tgz", + "integrity": "sha512-J8DJjgKgACIyCTbpfvoTnf7+ofTx1kxTGO7KAftkC+jczaMdQhKdgIBAg2DaYy+80cvYGTHy5q/HI9qMAwGbWw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/@atproto/xrpc": { + "version": "0.7.7", + "resolved": "https://registry.npmjs.org/@atproto/xrpc/-/xrpc-0.7.7.tgz", + "integrity": "sha512-K1ZyO/BU8JNtXX5dmPp7b5UrkLMMqpsIa/Lrj5D3Su+j1Xwq1m6QJ2XJ1AgjEjkI1v4Muzm7klianLE6XGxtmA==", + "license": "MIT", + "dependencies": { + "@atproto/lexicon": "^0.6.0", + "zod": "^3.23.8" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", + "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz", + "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", + "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz", + "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", + "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", + "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", + "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", + "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", + "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", + "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", + "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", + "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", + "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", + "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", + "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", + "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", + "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", + "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", + "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", + "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", + "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", + "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", + "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", + "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", + "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", + "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.0.tgz", + "integrity": "sha512-WOhNW9K8bR3kf4zLxbfg6Pxu2ybOUbB2AjMDHSQx86LIF4rH4Ft7vmMwNt0loO0eonglSNy4cpD3MKXXKQu0/A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.0.tgz", + "integrity": "sha512-u6JHLll5QKRvjciE78bQXDmqRqNs5M/3GVqZeMwvmjaNODJih/WIrJlFVEihvV0MiYFmd+ZyPr9wxOVbPAG2Iw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.0.tgz", + "integrity": "sha512-qEF7CsKKzSRc20Ciu2Zw1wRrBz4g56F7r/vRwY430UPp/nt1x21Q/fpJ9N5l47WWvJlkNCPJz3QRVw008fi7yA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.0.tgz", + "integrity": "sha512-WADYozJ4QCnXCH4wPB+3FuGmDPoFseVCUrANmA5LWwGmC6FL14BWC7pcq+FstOZv3baGX65tZ378uT6WG8ynTw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.0.tgz", + "integrity": "sha512-6b8wGHJlDrGeSE3aH5mGNHBjA0TTkxdoNHik5EkvPHCt351XnigA4pS7Wsj/Eo9Y8RBU6f35cjN9SYmCFBtzxw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.0.tgz", + "integrity": "sha512-h25Ga0t4jaylMB8M/JKAyrvvfxGRjnPQIR8lnCayyzEjEOx2EJIlIiMbhpWxDRKGKF8jbNH01NnN663dH638mA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.0.tgz", + "integrity": "sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.0.tgz", + "integrity": "sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.0.tgz", + "integrity": "sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.0.tgz", + "integrity": "sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.0.tgz", + "integrity": "sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.0.tgz", + "integrity": "sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.0.tgz", + "integrity": "sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.0.tgz", + "integrity": "sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.0.tgz", + "integrity": "sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.0.tgz", + "integrity": "sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.0.tgz", + "integrity": "sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.0.tgz", + "integrity": "sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.0.tgz", + "integrity": "sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.0.tgz", + "integrity": "sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.0.tgz", + "integrity": "sha512-pESDkos/PDzYwtyzB5p/UoNU/8fJo68vcXM9ZW2V0kjYayj1KaaUfi1NmTUTUpMn4UhU4gTuK8gIaFO4UGuMbA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.0.tgz", + "integrity": "sha512-hj1wFStD7B1YBeYmvY+lWXZ7ey73YGPcViMShYikqKT1GtstIKQAtfUI6yrzPjAy/O7pO0VLXGmUVWXQMaYgTQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.0.tgz", + "integrity": "sha512-SyaIPFoxmUPlNDq5EHkTbiKzmSEmq/gOYFI/3HHJ8iS/v1mbugVa7dXUzcJGQfoytp9DJFLhHH4U3/eTy2Bq4w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.0.tgz", + "integrity": "sha512-RdcryEfzZr+lAr5kRm2ucN9aVlCCa2QNq4hXelZxb8GG0NJSazq44Z3PCCc8wISRuCVnGs0lQJVX5Vp6fKA+IA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.0.tgz", + "integrity": "sha512-PrsWNQ8BuE00O3Xsx3ALh2Df8fAj9+cvvX9AIA6o4KpATR98c9mud4XtDWVvsEuyia5U4tVSTKygawyJkjm60w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", + "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.1.tgz", + "integrity": "sha512-xAV0fqBTk44Rn6SjJReEQkHP3RrqbJo6JQ4zZ7/uVOiJZRarBtblzrOfFIZeYUrukp2YD6snZG6IBqhOoHTm+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.1", + "@vitest/utils": "4.1.1", + "chai": "^6.2.2", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.1.tgz", + "integrity": "sha512-h3BOylsfsCLPeceuCPAAJ+BvNwSENgJa4hXoXu4im0bs9Lyp4URc4JYK4pWLZ4pG/UQn7AT92K6IByi6rE6g3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.1", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.1.tgz", + "integrity": "sha512-GM+TEQN5WhOygr1lp7skeVjdLPqqWMHsfzXrcHAqZJi/lIVh63H0kaRCY8MDhNWikx19zBUK8ceaLB7X5AH9NQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.1.tgz", + "integrity": "sha512-f7+FPy75vN91QGWsITueq0gedwUZy1fLtHOCMeQpjs8jTekAHeKP80zfDEnhrleviLHzVSDXIWuCIOFn3D3f8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.1", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.1.tgz", + "integrity": "sha512-kMVSgcegWV2FibXEx9p9WIKgje58lcTbXgnJixfcg15iK8nzCXhmalL0ZLtTWLW9PH1+1NEDShiFFedB3tEgWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.1", + "@vitest/utils": "4.1.1", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.1.tgz", + "integrity": "sha512-6Ti/KT5OVaiupdIZEuZN7l3CZcR0cxnxt70Z0//3CtwgObwA6jZhmVBA3yrXSVN3gmwjgd7oDNLlsXz526gpRA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.1.tgz", + "integrity": "sha512-cNxAlaB3sHoCdL6pj6yyUXv9Gry1NHNg0kFTXdvSIZXLHsqKH7chiWOkwJ5s5+d/oMwcoG9T0bKU38JZWKusrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.1", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/await-lock": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/await-lock/-/await-lock-2.2.2.tgz", + "integrity": "sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==", + "license": "MIT" + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/dotenv": { + "version": "17.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", + "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/es-module-lexer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz", + "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.4", + "@esbuild/android-arm": "0.27.4", + "@esbuild/android-arm64": "0.27.4", + "@esbuild/android-x64": "0.27.4", + "@esbuild/darwin-arm64": "0.27.4", + "@esbuild/darwin-x64": "0.27.4", + "@esbuild/freebsd-arm64": "0.27.4", + "@esbuild/freebsd-x64": "0.27.4", + "@esbuild/linux-arm": "0.27.4", + "@esbuild/linux-arm64": "0.27.4", + "@esbuild/linux-ia32": "0.27.4", + "@esbuild/linux-loong64": "0.27.4", + "@esbuild/linux-mips64el": "0.27.4", + "@esbuild/linux-ppc64": "0.27.4", + "@esbuild/linux-riscv64": "0.27.4", + "@esbuild/linux-s390x": "0.27.4", + "@esbuild/linux-x64": "0.27.4", + "@esbuild/netbsd-arm64": "0.27.4", + "@esbuild/netbsd-x64": "0.27.4", + "@esbuild/openbsd-arm64": "0.27.4", + "@esbuild/openbsd-x64": "0.27.4", + "@esbuild/openharmony-arm64": "0.27.4", + "@esbuild/sunos-x64": "0.27.4", + "@esbuild/win32-arm64": "0.27.4", + "@esbuild/win32-ia32": "0.27.4", + "@esbuild/win32-x64": "0.27.4" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.7", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz", + "integrity": "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/iso-datestring-validator": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/iso-datestring-validator/-/iso-datestring-validator-2.2.2.tgz", + "integrity": "sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA==", + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/multiformats": { + "version": "9.9.0", + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz", + "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==", + "license": "(Apache-2.0 AND MIT)" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/rollup": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.0.tgz", + "integrity": "sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.0", + "@rollup/rollup-android-arm64": "4.60.0", + "@rollup/rollup-darwin-arm64": "4.60.0", + "@rollup/rollup-darwin-x64": "4.60.0", + "@rollup/rollup-freebsd-arm64": "4.60.0", + "@rollup/rollup-freebsd-x64": "4.60.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.0", + "@rollup/rollup-linux-arm-musleabihf": "4.60.0", + "@rollup/rollup-linux-arm64-gnu": "4.60.0", + "@rollup/rollup-linux-arm64-musl": "4.60.0", + "@rollup/rollup-linux-loong64-gnu": "4.60.0", + "@rollup/rollup-linux-loong64-musl": "4.60.0", + "@rollup/rollup-linux-ppc64-gnu": "4.60.0", + "@rollup/rollup-linux-ppc64-musl": "4.60.0", + "@rollup/rollup-linux-riscv64-gnu": "4.60.0", + "@rollup/rollup-linux-riscv64-musl": "4.60.0", + "@rollup/rollup-linux-s390x-gnu": "4.60.0", + "@rollup/rollup-linux-x64-gnu": "4.60.0", + "@rollup/rollup-linux-x64-musl": "4.60.0", + "@rollup/rollup-openbsd-x64": "4.60.0", + "@rollup/rollup-openharmony-arm64": "4.60.0", + "@rollup/rollup-win32-arm64-msvc": "4.60.0", + "@rollup/rollup-win32-ia32-msvc": "4.60.0", + "@rollup/rollup-win32-x64-gnu": "4.60.0", + "@rollup/rollup-win32-x64-msvc": "4.60.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.0.0.tgz", + "integrity": "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz", + "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tlds": { + "version": "1.261.0", + "resolved": "https://registry.npmjs.org/tlds/-/tlds-1.261.0.tgz", + "integrity": "sha512-QXqwfEl9ddlGBaRFXIvNKK6OhipSiLXuRuLJX5DErz0o0Q0rYxulWLdFryTkV5PkdZct5iMInwYEGe/eR++1AA==", + "license": "MIT", + "bin": { + "tlds": "bin.js" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/twitter-api-v2": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/twitter-api-v2/-/twitter-api-v2-1.29.0.tgz", + "integrity": "sha512-v473q5bwme4N+DWSg6qY+JCvfg1nSJRWwui3HUALafxfqCvVkKiYmS/5x/pVeJwTmyeBxexMbzHwnzrH4h6oYQ==", + "license": "Apache-2.0" + }, + "node_modules/typescript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz", + "integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uint8arrays": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-3.0.0.tgz", + "integrity": "sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA==", + "license": "MIT", + "dependencies": { + "multiformats": "^9.4.2" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/unicode-segmenter": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/unicode-segmenter/-/unicode-segmenter-0.14.5.tgz", + "integrity": "sha512-jHGmj2LUuqDcX3hqY12Ql+uhUTn8huuxNZGq7GvtF6bSybzH3aFgedYu/KTzQStEgt1Ra2F3HxadNXsNjb3m3g==", + "license": "MIT" + }, + "node_modules/vite": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/vitest": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.1.tgz", + "integrity": "sha512-yF+o4POL41rpAzj5KVILUxm1GCjKnELvaqmU9TLLUbMfDzuN0UpUR9uaDs+mCtjPe+uYPksXDRLQGGPvj1cTmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.1", + "@vitest/mocker": "4.1.1", + "@vitest/pretty-format": "4.1.1", + "@vitest/runner": "4.1.1", + "@vitest/snapshot": "4.1.1", + "@vitest/spy": "4.1.1", + "@vitest/utils": "4.1.1", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.1", + "@vitest/browser-preview": "4.1.1", + "@vitest/browser-webdriverio": "4.1.1", + "@vitest/ui": "4.1.1", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yaml": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/social-cli/package.json b/social-cli/package.json new file mode 100644 index 0000000..f7a7a67 --- /dev/null +++ b/social-cli/package.json @@ -0,0 +1,37 @@ +{ + "name": "social-cli", + "version": "0.1.0", + "description": "Agent-optimized social media CLI. Bluesky + X.", + "type": "module", + "bin": { + "social-cli": "./dist/cli.js" + }, + "scripts": { + "build": "tsc", + "dev": "tsx src/cli.ts", + "test": "vitest" + }, + "keywords": [ + "social", + "cli", + "bluesky", + "atproto", + "twitter", + "agent" + ], + "license": "Apache-2.0", + "packageManager": "pnpm@10.20.0", + "dependencies": { + "@atproto/api": "^0.19.4", + "commander": "^14.0.3", + "dotenv": "^17.3.1", + "twitter-api-v2": "^1.29.0", + "yaml": "^2.8.3" + }, + "devDependencies": { + "@types/node": "^25.5.0", + "tsx": "^4.21.0", + "typescript": "^6.0.2", + "vitest": "^4.1.1" + } +} diff --git a/social-cli/pnpm-lock.yaml b/social-cli/pnpm-lock.yaml new file mode 100644 index 0000000..cd9f058 --- /dev/null +++ b/social-cli/pnpm-lock.yaml @@ -0,0 +1,1203 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@atproto/api': + specifier: ^0.19.4 + version: 0.19.4 + commander: + specifier: ^14.0.3 + version: 14.0.3 + dotenv: + specifier: ^17.3.1 + version: 17.3.1 + twitter-api-v2: + specifier: ^1.29.0 + version: 1.29.0 + yaml: + specifier: ^2.8.3 + version: 2.8.3 + devDependencies: + '@types/node': + specifier: ^25.5.0 + version: 25.5.0 + tsx: + specifier: ^4.21.0 + version: 4.21.0 + typescript: + specifier: ^6.0.2 + version: 6.0.2 + vitest: + specifier: ^4.1.1 + version: 4.1.1(@types/node@25.5.0)(vite@8.0.2(@types/node@25.5.0)(esbuild@0.27.4)(tsx@4.21.0)(yaml@2.8.3)) + +packages: + + '@atproto/api@0.19.4': + resolution: {integrity: sha512-fYNM62vdXxer0h8a9Jzl4/ag9uFIe0nTO+LkC6KTlx1yUDigrAoQMMbllIiCWj62GhUMxAkHabk/BZjjVAfKng==} + + '@atproto/common-web@0.4.19': + resolution: {integrity: sha512-3BTi58p5WpT+9/zb6UZrdsXcfPo5P45UJm0E4iwHLILr+jc37CuBj9JReDSZ4U0i9RTrI3ZkfySyZ9bd+LnMsw==} + + '@atproto/lex-data@0.0.14': + resolution: {integrity: sha512-53DUa9664SS76nGAMYopWsO10OH0AAdf7P/HSKB6Wzx3iqe6lk/K61QZnKxOG1LreYl5CfvIJU6eNf4txI6GlQ==} + + '@atproto/lex-json@0.0.14': + resolution: {integrity: sha512-6lPkDKqe7teEu4WrN5q7400cvZKgYS3uwUMvzG3F9XkgVYhOwSDCtouV/nSLBbpvo3l9OP0kiigtclcNcyekww==} + + '@atproto/lexicon@0.6.2': + resolution: {integrity: sha512-p3Ly6hinVZW0ETuAXZMeUGwuMm3g8HvQMQ41yyEE6AL0hAkfeKFaZKos6BdBrr6CjkpbrDZqE8M+5+QOceysMw==} + + '@atproto/syntax@0.5.1': + resolution: {integrity: sha512-J8DJjgKgACIyCTbpfvoTnf7+ofTx1kxTGO7KAftkC+jczaMdQhKdgIBAg2DaYy+80cvYGTHy5q/HI9qMAwGbWw==} + + '@atproto/xrpc@0.7.7': + resolution: {integrity: sha512-K1ZyO/BU8JNtXX5dmPp7b5UrkLMMqpsIa/Lrj5D3Su+j1Xwq1m6QJ2XJ1AgjEjkI1v4Muzm7klianLE6XGxtmA==} + + '@emnapi/core@1.9.1': + resolution: {integrity: sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==} + + '@emnapi/runtime@1.9.1': + resolution: {integrity: sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==} + + '@emnapi/wasi-threads@1.2.0': + resolution: {integrity: sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==} + + '@esbuild/aix-ppc64@0.27.4': + resolution: {integrity: sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.4': + resolution: {integrity: sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.4': + resolution: {integrity: sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.4': + resolution: {integrity: sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.4': + resolution: {integrity: sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.4': + resolution: {integrity: sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.4': + resolution: {integrity: sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.4': + resolution: {integrity: sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.4': + resolution: {integrity: sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.4': + resolution: {integrity: sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.4': + resolution: {integrity: sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.4': + resolution: {integrity: sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.4': + resolution: {integrity: sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.4': + resolution: {integrity: sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.4': + resolution: {integrity: sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.4': + resolution: {integrity: sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.4': + resolution: {integrity: sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.4': + resolution: {integrity: sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.4': + resolution: {integrity: sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.4': + resolution: {integrity: sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.4': + resolution: {integrity: sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.4': + resolution: {integrity: sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.4': + resolution: {integrity: sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.4': + resolution: {integrity: sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.4': + resolution: {integrity: sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.4': + resolution: {integrity: sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@napi-rs/wasm-runtime@1.1.1': + resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==} + + '@oxc-project/types@0.122.0': + resolution: {integrity: sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==} + + '@rolldown/binding-android-arm64@1.0.0-rc.11': + resolution: {integrity: sha512-SJ+/g+xNnOh6NqYxD0V3uVN4W3VfnrGsC9/hoglicgTNfABFG9JjISvkkU0dNY84MNHLWyOgxP9v9Y9pX4S7+A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.0-rc.11': + resolution: {integrity: sha512-7WQgR8SfOPwmDZGFkThUvsmd/nwAWv91oCO4I5LS7RKrssPZmOt7jONN0cW17ydGC1n/+puol1IpoieKqQidmg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.0-rc.11': + resolution: {integrity: sha512-39Ks6UvIHq4rEogIfQBoBRusj0Q0nPVWIvqmwBLaT6aqQGIakHdESBVOPRRLacy4WwUPIx4ZKzfZ9PMW+IeyUQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.0-rc.11': + resolution: {integrity: sha512-jfsm0ZHfhiqrvWjJAmzsqiIFPz5e7mAoCOPBNTcNgkiid/LaFKiq92+0ojH+nmJmKYkre4t71BWXUZDNp7vsag==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.11': + resolution: {integrity: sha512-zjQaUtSyq1nVe3nxmlSCuR96T1LPlpvmJ0SZy0WJFEsV4kFbXcq2u68L4E6O0XeFj4aex9bEauqjW8UQBeAvfQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.11': + resolution: {integrity: sha512-WMW1yE6IOnehTcFE9eipFkm3XN63zypWlrJQ2iF7NrQ9b2LDRjumFoOGJE8RJJTJCTBAdmLMnJ8uVitACUUo1Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.11': + resolution: {integrity: sha512-jfndI9tsfm4APzjNt6QdBkYwre5lRPUgHeDHoI7ydKUuJvz3lZeCfMsI56BZj+7BYqiKsJm7cfd/6KYV7ubrBg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.11': + resolution: {integrity: sha512-ZlFgw46NOAGMgcdvdYwAGu2Q+SLFA9LzbJLW+iyMOJyhj5wk6P3KEE9Gct4xWwSzFoPI7JCdYmYMzVtlgQ+zfw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.11': + resolution: {integrity: sha512-hIOYmuT6ofM4K04XAZd3OzMySEO4K0/nc9+jmNcxNAxRi6c5UWpqfw3KMFV4MVFWL+jQsSh+bGw2VqmaPMTLyw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.11': + resolution: {integrity: sha512-qXBQQO9OvkjjQPLdUVr7Nr2t3QTZI7s4KZtfw7HzBgjbmAPSFwSv4rmET9lLSgq3rH/ndA3ngv3Qb8l2njoPNA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.11': + resolution: {integrity: sha512-/tpFfoSTzUkH9LPY+cYbqZBDyyX62w5fICq9qzsHLL8uTI6BHip3Q9Uzft0wylk/i8OOwKik8OxW+QAhDmzwmg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.11': + resolution: {integrity: sha512-mcp3Rio2w72IvdZG0oQ4bM2c2oumtwHfUfKncUM6zGgz0KgPz4YmDPQfnXEiY5t3+KD/i8HG2rOB/LxdmieK2g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.11': + resolution: {integrity: sha512-LXk5Hii1Ph9asuGRjBuz8TUxdc1lWzB7nyfdoRgI0WGPZKmCxvlKk8KfYysqtr4MfGElu/f/pEQRh8fcEgkrWw==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.11': + resolution: {integrity: sha512-dDwf5otnx0XgRY1yqxOC4ITizcdzS/8cQ3goOWv3jFAo4F+xQYni+hnMuO6+LssHHdJW7+OCVL3CoU4ycnh35Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.11': + resolution: {integrity: sha512-LN4/skhSggybX71ews7dAj6r2geaMJfm3kMbK2KhFMg9B10AZXnKoLCVVgzhMHL0S+aKtr4p8QbAW8k+w95bAA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.0-rc.11': + resolution: {integrity: sha512-xQO9vbwBecJRv9EUcQ/y0dzSTJgA7Q6UVN7xp6B81+tBGSLVAK03yJ9NkJaUA7JFD91kbjxRSC/mDnmvXzbHoQ==} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@tybys/wasm-util@0.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/node@25.5.0': + resolution: {integrity: sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==} + + '@vitest/expect@4.1.1': + resolution: {integrity: sha512-xAV0fqBTk44Rn6SjJReEQkHP3RrqbJo6JQ4zZ7/uVOiJZRarBtblzrOfFIZeYUrukp2YD6snZG6IBqhOoHTm+A==} + + '@vitest/mocker@4.1.1': + resolution: {integrity: sha512-h3BOylsfsCLPeceuCPAAJ+BvNwSENgJa4hXoXu4im0bs9Lyp4URc4JYK4pWLZ4pG/UQn7AT92K6IByi6rE6g3A==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.1': + resolution: {integrity: sha512-GM+TEQN5WhOygr1lp7skeVjdLPqqWMHsfzXrcHAqZJi/lIVh63H0kaRCY8MDhNWikx19zBUK8ceaLB7X5AH9NQ==} + + '@vitest/runner@4.1.1': + resolution: {integrity: sha512-f7+FPy75vN91QGWsITueq0gedwUZy1fLtHOCMeQpjs8jTekAHeKP80zfDEnhrleviLHzVSDXIWuCIOFn3D3f8A==} + + '@vitest/snapshot@4.1.1': + resolution: {integrity: sha512-kMVSgcegWV2FibXEx9p9WIKgje58lcTbXgnJixfcg15iK8nzCXhmalL0ZLtTWLW9PH1+1NEDShiFFedB3tEgWg==} + + '@vitest/spy@4.1.1': + resolution: {integrity: sha512-6Ti/KT5OVaiupdIZEuZN7l3CZcR0cxnxt70Z0//3CtwgObwA6jZhmVBA3yrXSVN3gmwjgd7oDNLlsXz526gpRA==} + + '@vitest/utils@4.1.1': + resolution: {integrity: sha512-cNxAlaB3sHoCdL6pj6yyUXv9Gry1NHNg0kFTXdvSIZXLHsqKH7chiWOkwJ5s5+d/oMwcoG9T0bKU38JZWKusrQ==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + await-lock@2.2.2: + resolution: {integrity: sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==} + + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + + commander@14.0.3: + resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} + engines: {node: '>=20'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + dotenv@17.3.1: + resolution: {integrity: sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==} + engines: {node: '>=12'} + + es-module-lexer@2.0.0: + resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==} + + esbuild@0.27.4: + resolution: {integrity: sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==} + engines: {node: '>=18'} + hasBin: true + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + get-tsconfig@4.13.7: + resolution: {integrity: sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==} + + iso-datestring-validator@2.2.2: + resolution: {integrity: sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA==} + + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + multiformats@9.9.0: + resolution: {integrity: sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + postcss@8.5.8: + resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} + engines: {node: ^10 || ^12 || >=14} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + rolldown@1.0.0-rc.11: + resolution: {integrity: sha512-NRjoKMusSjfRbSYiH3VSumlkgFe7kYAa3pzVOsVYVFY3zb5d7nS+a3KGQ7hJKXuYWbzJKPVQ9Wxq2UvyK+ENpw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@4.0.0: + resolution: {integrity: sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.0.4: + resolution: {integrity: sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==} + engines: {node: '>=18'} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} + engines: {node: '>=14.0.0'} + + tlds@1.261.0: + resolution: {integrity: sha512-QXqwfEl9ddlGBaRFXIvNKK6OhipSiLXuRuLJX5DErz0o0Q0rYxulWLdFryTkV5PkdZct5iMInwYEGe/eR++1AA==} + hasBin: true + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsx@4.21.0: + resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} + engines: {node: '>=18.0.0'} + hasBin: true + + twitter-api-v2@1.29.0: + resolution: {integrity: sha512-v473q5bwme4N+DWSg6qY+JCvfg1nSJRWwui3HUALafxfqCvVkKiYmS/5x/pVeJwTmyeBxexMbzHwnzrH4h6oYQ==} + + typescript@6.0.2: + resolution: {integrity: sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==} + engines: {node: '>=14.17'} + hasBin: true + + uint8arrays@3.0.0: + resolution: {integrity: sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA==} + + undici-types@7.18.2: + resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + + unicode-segmenter@0.14.5: + resolution: {integrity: sha512-jHGmj2LUuqDcX3hqY12Ql+uhUTn8huuxNZGq7GvtF6bSybzH3aFgedYu/KTzQStEgt1Ra2F3HxadNXsNjb3m3g==} + + vite@8.0.2: + resolution: {integrity: sha512-1gFhNi+bHhRE/qKZOJXACm6tX4bA3Isy9KuKF15AgSRuRazNBOJfdDemPBU16/mpMxApDPrWvZ08DcLPEoRnuA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.1.0 + esbuild: ^0.27.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@4.1.1: + resolution: {integrity: sha512-yF+o4POL41rpAzj5KVILUxm1GCjKnELvaqmU9TLLUbMfDzuN0UpUR9uaDs+mCtjPe+uYPksXDRLQGGPvj1cTmA==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.1 + '@vitest/browser-preview': 4.1.1 + '@vitest/browser-webdriverio': 4.1.1 + '@vitest/ui': 4.1.1 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + yaml@2.8.3: + resolution: {integrity: sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==} + engines: {node: '>= 14.6'} + hasBin: true + + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + +snapshots: + + '@atproto/api@0.19.4': + dependencies: + '@atproto/common-web': 0.4.19 + '@atproto/lexicon': 0.6.2 + '@atproto/syntax': 0.5.1 + '@atproto/xrpc': 0.7.7 + await-lock: 2.2.2 + multiformats: 9.9.0 + tlds: 1.261.0 + zod: 3.25.76 + + '@atproto/common-web@0.4.19': + dependencies: + '@atproto/lex-data': 0.0.14 + '@atproto/lex-json': 0.0.14 + '@atproto/syntax': 0.5.1 + zod: 3.25.76 + + '@atproto/lex-data@0.0.14': + dependencies: + multiformats: 9.9.0 + tslib: 2.8.1 + uint8arrays: 3.0.0 + unicode-segmenter: 0.14.5 + + '@atproto/lex-json@0.0.14': + dependencies: + '@atproto/lex-data': 0.0.14 + tslib: 2.8.1 + + '@atproto/lexicon@0.6.2': + dependencies: + '@atproto/common-web': 0.4.19 + '@atproto/syntax': 0.5.1 + iso-datestring-validator: 2.2.2 + multiformats: 9.9.0 + zod: 3.25.76 + + '@atproto/syntax@0.5.1': + dependencies: + tslib: 2.8.1 + + '@atproto/xrpc@0.7.7': + dependencies: + '@atproto/lexicon': 0.6.2 + zod: 3.25.76 + + '@emnapi/core@1.9.1': + dependencies: + '@emnapi/wasi-threads': 1.2.0 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.9.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild/aix-ppc64@0.27.4': + optional: true + + '@esbuild/android-arm64@0.27.4': + optional: true + + '@esbuild/android-arm@0.27.4': + optional: true + + '@esbuild/android-x64@0.27.4': + optional: true + + '@esbuild/darwin-arm64@0.27.4': + optional: true + + '@esbuild/darwin-x64@0.27.4': + optional: true + + '@esbuild/freebsd-arm64@0.27.4': + optional: true + + '@esbuild/freebsd-x64@0.27.4': + optional: true + + '@esbuild/linux-arm64@0.27.4': + optional: true + + '@esbuild/linux-arm@0.27.4': + optional: true + + '@esbuild/linux-ia32@0.27.4': + optional: true + + '@esbuild/linux-loong64@0.27.4': + optional: true + + '@esbuild/linux-mips64el@0.27.4': + optional: true + + '@esbuild/linux-ppc64@0.27.4': + optional: true + + '@esbuild/linux-riscv64@0.27.4': + optional: true + + '@esbuild/linux-s390x@0.27.4': + optional: true + + '@esbuild/linux-x64@0.27.4': + optional: true + + '@esbuild/netbsd-arm64@0.27.4': + optional: true + + '@esbuild/netbsd-x64@0.27.4': + optional: true + + '@esbuild/openbsd-arm64@0.27.4': + optional: true + + '@esbuild/openbsd-x64@0.27.4': + optional: true + + '@esbuild/openharmony-arm64@0.27.4': + optional: true + + '@esbuild/sunos-x64@0.27.4': + optional: true + + '@esbuild/win32-arm64@0.27.4': + optional: true + + '@esbuild/win32-ia32@0.27.4': + optional: true + + '@esbuild/win32-x64@0.27.4': + optional: true + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@napi-rs/wasm-runtime@1.1.1': + dependencies: + '@emnapi/core': 1.9.1 + '@emnapi/runtime': 1.9.1 + '@tybys/wasm-util': 0.10.1 + optional: true + + '@oxc-project/types@0.122.0': {} + + '@rolldown/binding-android-arm64@1.0.0-rc.11': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.0-rc.11': + optional: true + + '@rolldown/binding-darwin-x64@1.0.0-rc.11': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.0-rc.11': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.11': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.11': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.11': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.11': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.11': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.11': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.11': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.11': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.11': + dependencies: + '@napi-rs/wasm-runtime': 1.1.1 + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.11': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.11': + optional: true + + '@rolldown/pluginutils@1.0.0-rc.11': {} + + '@standard-schema/spec@1.1.0': {} + + '@tybys/wasm-util@0.10.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.8': {} + + '@types/node@25.5.0': + dependencies: + undici-types: 7.18.2 + + '@vitest/expect@4.1.1': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.1 + '@vitest/utils': 4.1.1 + chai: 6.2.2 + tinyrainbow: 3.1.0 + + '@vitest/mocker@4.1.1(vite@8.0.2(@types/node@25.5.0)(esbuild@0.27.4)(tsx@4.21.0)(yaml@2.8.3))': + dependencies: + '@vitest/spy': 4.1.1 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.0.2(@types/node@25.5.0)(esbuild@0.27.4)(tsx@4.21.0)(yaml@2.8.3) + + '@vitest/pretty-format@4.1.1': + dependencies: + tinyrainbow: 3.1.0 + + '@vitest/runner@4.1.1': + dependencies: + '@vitest/utils': 4.1.1 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.1': + dependencies: + '@vitest/pretty-format': 4.1.1 + '@vitest/utils': 4.1.1 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.1': {} + + '@vitest/utils@4.1.1': + dependencies: + '@vitest/pretty-format': 4.1.1 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 + + assertion-error@2.0.1: {} + + await-lock@2.2.2: {} + + chai@6.2.2: {} + + commander@14.0.3: {} + + convert-source-map@2.0.0: {} + + detect-libc@2.1.2: {} + + dotenv@17.3.1: {} + + es-module-lexer@2.0.0: {} + + esbuild@0.27.4: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.4 + '@esbuild/android-arm': 0.27.4 + '@esbuild/android-arm64': 0.27.4 + '@esbuild/android-x64': 0.27.4 + '@esbuild/darwin-arm64': 0.27.4 + '@esbuild/darwin-x64': 0.27.4 + '@esbuild/freebsd-arm64': 0.27.4 + '@esbuild/freebsd-x64': 0.27.4 + '@esbuild/linux-arm': 0.27.4 + '@esbuild/linux-arm64': 0.27.4 + '@esbuild/linux-ia32': 0.27.4 + '@esbuild/linux-loong64': 0.27.4 + '@esbuild/linux-mips64el': 0.27.4 + '@esbuild/linux-ppc64': 0.27.4 + '@esbuild/linux-riscv64': 0.27.4 + '@esbuild/linux-s390x': 0.27.4 + '@esbuild/linux-x64': 0.27.4 + '@esbuild/netbsd-arm64': 0.27.4 + '@esbuild/netbsd-x64': 0.27.4 + '@esbuild/openbsd-arm64': 0.27.4 + '@esbuild/openbsd-x64': 0.27.4 + '@esbuild/openharmony-arm64': 0.27.4 + '@esbuild/sunos-x64': 0.27.4 + '@esbuild/win32-arm64': 0.27.4 + '@esbuild/win32-ia32': 0.27.4 + '@esbuild/win32-x64': 0.27.4 + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + expect-type@1.3.0: {} + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + fsevents@2.3.3: + optional: true + + get-tsconfig@4.13.7: + dependencies: + resolve-pkg-maps: 1.0.0 + + iso-datestring-validator@2.2.2: {} + + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + multiformats@9.9.0: {} + + nanoid@3.3.11: {} + + obug@2.1.1: {} + + pathe@2.0.3: {} + + picocolors@1.1.1: {} + + picomatch@4.0.4: {} + + postcss@8.5.8: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + resolve-pkg-maps@1.0.0: {} + + rolldown@1.0.0-rc.11: + dependencies: + '@oxc-project/types': 0.122.0 + '@rolldown/pluginutils': 1.0.0-rc.11 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.0-rc.11 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.11 + '@rolldown/binding-darwin-x64': 1.0.0-rc.11 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.11 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.11 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.11 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.11 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.11 + '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.11 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.11 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.11 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.11 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.11 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.11 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.11 + + siginfo@2.0.0: {} + + source-map-js@1.2.1: {} + + stackback@0.0.2: {} + + std-env@4.0.0: {} + + tinybench@2.9.0: {} + + tinyexec@1.0.4: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + tinyrainbow@3.1.0: {} + + tlds@1.261.0: {} + + tslib@2.8.1: {} + + tsx@4.21.0: + dependencies: + esbuild: 0.27.4 + get-tsconfig: 4.13.7 + optionalDependencies: + fsevents: 2.3.3 + + twitter-api-v2@1.29.0: {} + + typescript@6.0.2: {} + + uint8arrays@3.0.0: + dependencies: + multiformats: 9.9.0 + + undici-types@7.18.2: {} + + unicode-segmenter@0.14.5: {} + + vite@8.0.2(@types/node@25.5.0)(esbuild@0.27.4)(tsx@4.21.0)(yaml@2.8.3): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.8 + rolldown: 1.0.0-rc.11 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 25.5.0 + esbuild: 0.27.4 + fsevents: 2.3.3 + tsx: 4.21.0 + yaml: 2.8.3 + + vitest@4.1.1(@types/node@25.5.0)(vite@8.0.2(@types/node@25.5.0)(esbuild@0.27.4)(tsx@4.21.0)(yaml@2.8.3)): + dependencies: + '@vitest/expect': 4.1.1 + '@vitest/mocker': 4.1.1(vite@8.0.2(@types/node@25.5.0)(esbuild@0.27.4)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/pretty-format': 4.1.1 + '@vitest/runner': 4.1.1 + '@vitest/snapshot': 4.1.1 + '@vitest/spy': 4.1.1 + '@vitest/utils': 4.1.1 + es-module-lexer: 2.0.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 4.0.0 + tinybench: 2.9.0 + tinyexec: 1.0.4 + tinyglobby: 0.2.15 + tinyrainbow: 3.1.0 + vite: 8.0.2(@types/node@25.5.0)(esbuild@0.27.4)(tsx@4.21.0)(yaml@2.8.3) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 25.5.0 + transitivePeerDependencies: + - msw + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + yaml@2.8.3: {} + + zod@3.25.76: {} diff --git a/social-cli/processed.yaml b/social-cli/processed.yaml new file mode 100644 index 0000000..d2f6c7c --- /dev/null +++ b/social-cli/processed.yaml @@ -0,0 +1,73 @@ +processed: + - at://did:plc:gfrmhdmjvxn2sjedzboeudef/app.bsky.feed.post/3mhvr2xugik2s + - at://did:plc:k7wclckeajmuibxbamtbejjg/app.bsky.feed.post/3mhvqoqiwk22m + - at://did:plc:gfrmhdmjvxn2sjedzboeudef/app.bsky.feed.post/3mhtssxf7ps2e + - at://did:plc:gfrmhdmjvxn2sjedzboeudef/app.bsky.feed.post/3mhtuksubtk2z + - at://did:plc:gfrmhdmjvxn2sjedzboeudef/app.bsky.feed.post/3mhtuokmpjs2z + - at://did:plc:gfrmhdmjvxn2sjedzboeudef/app.bsky.feed.post/3mhtup4nmhs2z + - at://did:plc:gfrmhdmjvxn2sjedzboeudef/app.bsky.feed.post/3mhtuu752ck2z + - at://did:plc:gfrmhdmjvxn2sjedzboeudef/app.bsky.feed.post/3mhtuww7bic2z + - at://did:plc:gfrmhdmjvxn2sjedzboeudef/app.bsky.feed.post/3mhtwhr2dy224 + - at://did:plc:mxzuau6m53jtdsbqe6f4laov/app.bsky.feed.post/3mhty7cxc722r + - at://did:plc:mxzuau6m53jtdsbqe6f4laov/app.bsky.feed.post/3mhty7d2cus2r + - at://did:plc:mxzuau6m53jtdsbqe6f4laov/app.bsky.feed.post/3mhty7ddslk2r + - at://did:plc:gfrmhdmjvxn2sjedzboeudef/app.bsky.feed.post/3mhtygvmqdk24 + - at://did:plc:6xteynpx4dgj3vbgnkth4wr2/app.bsky.graph.follow/3mhwpuymw6q2a + - at://did:plc:yijxnx3vxxr7nzjsat5tlbh6/app.bsky.graph.follow/3mhu3kaeg522a + - at://did:plc:mov3spywnp5iakegklmcptkk/app.bsky.graph.follow/3mhtyyoozvf2g + - at://did:plc:lchy7sgr7rl42qqzquljpdbq/app.bsky.graph.follow/3mhtymra5hq2k + - at://did:plc:yijxnx3vxxr7nzjsat5tlbh6/app.bsky.feed.post/3mhu3kaioj52i + - at://did:plc:k7wclckeajmuibxbamtbejjg/app.bsky.feed.post/3mhvd3fituc2g + - at://did:plc:mxzuau6m53jtdsbqe6f4laov/app.bsky.feed.post/3mhtwgx2zgs2r + - at://did:plc:yijxnx3vxxr7nzjsat5tlbh6/app.bsky.feed.post/3mhxdtjtybj2x + - at://did:plc:4fnjgkok76q27hep7z6bfy7v/app.bsky.graph.follow/3mhxsnn6w3v2u + - at://did:plc:yijxnx3vxxr7nzjsat5tlbh6/app.bsky.graph.follow/3mhxdtjobac2n + - "2037345084421849530" + - "2036586733442310629" + - "2036175299831013535" + - "2031967777204957328" + - "2031966899631014158" + - "2027517326506266825" + - "2027517075783299402" + - "2027516308552880158" + - "2027514572031365391" + - "2027514567371460902" + - "2027513936107720953" + - "2027513265463636422" + - "2027512531405996343" + - "2027512456755695784" + - "2027511478425948609" + - "2027508153018159321" + - "2027507933152743783" + - "2027507784489566410" + - "2027507279030063335" + - "2027507184679203235" + - "2027507098775679147" + - "2027506612215419339" + - "2027506072282656942" + - "2027505271212544444" + - "2027505073522094499" + - "2027504868508758195" + - "2027504839266341138" + - "2027504565424115887" + - "2027504223311499651" + - "2027504113307816137" + - "2027503807601549701" + - "2027503587828339069" + - "2027503475295125601" + - "2027503338229567969" + - "2027503292226671062" + - "2027503209888076010" + - "2027503000520978770" + - "2027502507833102411" + - "2027501877982859453" + - "2027501145359585365" + - "2027501021933527434" + - "2027501001226523039" + - "2027500741674557667" + - "2027499295021404456" + - "2027498965986660715" + - "2027498653406163263" + - "2027498353475653714" + - "2027498306071384294" + - "2027497540581814519" diff --git a/social-cli/src/cli.ts b/social-cli/src/cli.ts new file mode 100644 index 0000000..911f817 --- /dev/null +++ b/social-cli/src/cli.ts @@ -0,0 +1,277 @@ +#!/usr/bin/env node + +/** + * social-cli: Agent-optimized social media CLI. + * Bluesky + X. YAML in, YAML out. Exit codes for agents. + */ + +import { Command } from "commander" + +const program = new Command() + .name("social-cli") + .description("Agent-optimized social media CLI") + .version("0.1.0") + +// sync: Fetch notifications → inbox.yaml +program + .command("sync") + .description("Fetch notifications from platforms → inbox.yaml") + .option("-p, --platform ", "Platforms to sync (default: all)") + .option("--unread-only", "Only fetch unread notifications", true) + .option("-n, --limit ", "Max notifications per platform", "50") + .option("-o, --output ", "Output file", "inbox.yaml") + .option("--max-items ", "Max inbox items before truncating oldest", "200") + .option("--users-dir ", "Directory of user memory files for context enrichment") + .option("--reset", "Clear cursors and re-fetch all notifications from scratch") + .option("--clear", "Clear both cursors and the local inbox for a fully fresh start") + .action(async (opts) => { + const { sync } = await import("./commands/sync.js") + await sync({ + platforms: opts.platform, + unreadOnly: opts.unreadOnly, + limit: parseInt(opts.limit), + output: opts.output, + maxItems: parseInt(opts.maxItems), + usersDir: opts.usersDir, + reset: opts.reset, + clear: opts.clear, + }) + }) + +// dispatch: outbox.yaml → post to platforms +program + .command("dispatch") + .description("Dispatch posts from outbox YAML") + .argument("[file]", "Outbox file", "outbox.yaml") + .option("--dry-run", "Validate only, don't post") + .action(async (file, opts) => { + const { dispatch } = await import("./commands/dispatch.js") + await dispatch({ file, dryRun: opts.dryRun }) + }) + +// check: Anything actionable? Exit code only. +program + .command("check") + .description("Check if inbox has actionable items (exit 0 = yes, 1 = no)") + .option("-t, --threshold ", "Minimum items to trigger", "1") + .action(async (opts) => { + const { check } = await import("./commands/check.js") + await check({ threshold: parseInt(opts.threshold) }) + }) + +// search: Search posts +program + .command("search") + .description("Search posts on a platform") + .argument("", "Search query") + .option("-p, --platform ", "Platform", "bsky") + .option("-n, --limit ", "Max results", "10") + .action(async (query, opts) => { + const { search } = await import("./commands/search.js") + await search(query, { + platform: opts.platform, + limit: parseInt(opts.limit), + }) + }) + +// feed: Read timeline +program + .command("feed") + .description("Fetch timeline → feed.yaml") + .option("-p, --platform ", "Platform", "bsky") + .option("-n, --limit ", "Max posts", "50") + .option("-o, --output ", "Output file", "feed.yaml") + .action(async (opts) => { + const { feed } = await import("./commands/feed.js") + await feed({ + platform: opts.platform, + limit: parseInt(opts.limit), + output: opts.output, + }) + }) + +// post: Quick single post +program + .command("post") + .description("Post to a platform") + .argument("", "Text to post") + .option("-p, --platform ", "Platform", "bsky") + .option("--quote ", "Quote/repost a post by ID") + .action(async (text, opts) => { + const { validateText } = await import("./util/validate.js") + validateText(opts.platform, text) + const { getPlatformAsync } = await import("./platforms/index.js") + const platform = await getPlatformAsync(opts.platform) + const result = await platform.post(text, { quoteId: opts.quote }) + console.log(`Posted: ${result.id}`) + }) + +// reply: Quick reply +program + .command("reply") + .description("Reply to a post") + .argument("", "Reply text") + .requiredOption("--id ", "Post ID to reply to") + .option("-p, --platform ", "Platform", "bsky") + .action(async (text, opts) => { + const { validateText } = await import("./util/validate.js") + validateText(opts.platform, text) + const { getPlatformAsync } = await import("./platforms/index.js") + const platform = await getPlatformAsync(opts.platform) + const result = await platform.reply(opts.id, text) + console.log(`Replied: ${result.id}`) + }) + +// thread: Post a thread +program + .command("thread") + .description("Post a thread") + .argument("", "Thread posts (each argument is one post)") + .option("-p, --platform ", "Platform", "bsky") + .action(async (posts, opts) => { + const { validateTexts } = await import("./util/validate.js") + validateTexts(opts.platform, posts) + const { getPlatformAsync } = await import("./platforms/index.js") + const platform = await getPlatformAsync(opts.platform) + const results = await platform.thread(posts) + for (const r of results) console.log(`Posted: ${r.id}`) + console.log(`Thread: ${results.length} posts`) + }) + +// annotate: Attach annotation to a post +program + .command("annotate") + .description("Annotate a URL or post (Bluesky only)") + .argument("", "Annotation text") + .requiredOption("--target ", "URL or AT-URI to annotate") + .option("-p, --platform ", "Platform", "bsky") + .option("--motivation ", "W3C motivation", "commenting") + .option("--quote ", "Exact text to anchor to") + .action(async (text, opts) => { + const { annotate } = await import("./commands/annotate.js") + await annotate({ + platform: opts.platform, + id: opts.target, + text, + motivation: opts.motivation, + quote: opts.quote, + }) + }) + +// rate-limits: Show rate limit status +program + .command("rate-limits") + .description("Show rate limit status") + .option("-p, --platform ", "Platforms (default: all)") + .action(async (opts) => { + const { getPlatformAsync, availablePlatforms } = await import("./platforms/index.js") + const { stringify } = await import("yaml") + const platforms = opts.platform ?? availablePlatforms() + const limits = [] + for (const name of platforms) { + try { + const p = await getPlatformAsync(name) + limits.push(await p.rateLimitStatus()) + } catch { + // skip unavailable + } + } + process.stdout.write(stringify(limits)) + }) + +// delete: Delete a post +program + .command("delete") + .description("Delete a post by ID/URI") + .argument("", "Post ID or AT-URI") + .option("-p, --platform ", "Platform", "bsky") + .action(async (id, opts) => { + const { getPlatformAsync } = await import("./platforms/index.js") + const platform = await getPlatformAsync(opts.platform) + if (!platform.delete) { + console.error(`Platform ${opts.platform} does not support delete`) + process.exit(1) + } + await platform.delete(id) + console.log(`Deleted: ${id}`) + }) + +// like: Like a post +program + .command("like") + .description("Like a post by ID/URI") + .argument("", "Post ID or AT-URI") + .option("-p, --platform ", "Platform", "bsky") + .action(async (id, opts) => { + const { getPlatformAsync } = await import("./platforms/index.js") + const platform = await getPlatformAsync(opts.platform) + if (!platform.like) { + console.error(`Platform ${opts.platform} does not support like`) + process.exit(1) + } + await platform.like(id) + console.log(`Liked: ${id}`) + }) + +// whoami: Show current account info +program + .command("whoami") + .description("Show current account info") + .option("-p, --platform ", "Platforms (default: all)") + .action(async (opts) => { + const { getPlatformAsync, availablePlatforms } = await import("./platforms/index.js") + const { stringify } = await import("yaml") + const platforms = opts.platform ?? availablePlatforms() + const profiles = [] + for (const name of platforms) { + try { + const p = await getPlatformAsync(name) + if (p.whoami) profiles.push(await p.whoami()) + } catch { + // skip unavailable + } + } + process.stdout.write(stringify(profiles)) + }) + +// posts: Fetch a user's recent posts +program + .command("posts") + .description("Fetch recent posts by a user") + .argument("", "User handle (e.g. cameron.stream, @cameron_pfiffer)") + .option("-p, --platform ", "Platform", "bsky") + .option("-n, --limit ", "Max posts", "20") + .action(async (handle, opts) => { + const { getPlatformAsync } = await import("./platforms/index.js") + const { stringify } = await import("yaml") + const platform = await getPlatformAsync(opts.platform) + if (!platform.userPosts) { + console.error(`Platform ${opts.platform} does not support user posts`) + process.exit(1) + } + const cleanHandle = handle.replace(/^@/, "") + const posts = await platform.userPosts(cleanHandle, parseInt(opts.limit)) + process.stdout.write(stringify(posts, { lineWidth: 120 })) + }) + +// profile: Look up a user +program + .command("profile") + .description("Look up a user by handle") + .argument("", "User handle (e.g. cameron.stream, @cameron_pfiffer)") + .option("-p, --platform ", "Platform", "bsky") + .action(async (handle, opts) => { + const { getPlatformAsync } = await import("./platforms/index.js") + const { stringify } = await import("yaml") + const platform = await getPlatformAsync(opts.platform) + if (!platform.profile) { + console.error(`Platform ${opts.platform} does not support profile lookup`) + process.exit(1) + } + // Strip leading @ if present + const cleanHandle = handle.replace(/^@/, "") + const info = await platform.profile(cleanHandle) + process.stdout.write(stringify(info)) + }) + +program.parse() diff --git a/social-cli/src/commands/annotate.ts b/social-cli/src/commands/annotate.ts new file mode 100644 index 0000000..348c577 --- /dev/null +++ b/social-cli/src/commands/annotate.ts @@ -0,0 +1,28 @@ +/** + * annotate: Attach an annotation to a post. + * Currently Bluesky-only (at.margin.annotation). + */ + +import { getPlatformAsync } from "../platforms/index.js" + +export async function annotate(opts: { + platform: string + id: string + text: string + motivation?: string + quote?: string +}): Promise { + const platform = await getPlatformAsync(opts.platform) + + if (!platform.annotate) { + console.error(`Platform ${opts.platform} does not support annotations`) + process.exit(1) + } + + const result = await platform.annotate(opts.id, opts.text, { + motivation: opts.motivation, + quote: opts.quote, + }) + + console.log(`Annotated: ${result.id}`) +} diff --git a/social-cli/src/commands/check.ts b/social-cli/src/commands/check.ts new file mode 100644 index 0000000..1d6a9bb --- /dev/null +++ b/social-cli/src/commands/check.ts @@ -0,0 +1,28 @@ +/** + * check: Is there anything actionable? + * Exit 0 = yes, exit 1 = no. No stdout. + * Agents use exit code to decide whether to process. + */ + +import { readFileSync, existsSync } from "node:fs" +import { resolve } from "node:path" +import { parse } from "yaml" + +export async function check(opts: { + threshold?: number +}): Promise { + const threshold = opts.threshold ?? 1 + const inboxPath = resolve(process.cwd(), "inbox.yaml") + + if (!existsSync(inboxPath)) { + process.exit(1) + } + + try { + const raw = parse(readFileSync(inboxPath, "utf-8")) as { notifications?: any[] } + const count = raw?.notifications?.length ?? 0 + process.exit(count >= threshold ? 0 : 1) + } catch { + process.exit(1) + } +} diff --git a/social-cli/src/commands/dispatch.ts b/social-cli/src/commands/dispatch.ts new file mode 100644 index 0000000..41024af --- /dev/null +++ b/social-cli/src/commands/dispatch.ts @@ -0,0 +1,276 @@ +/** + * dispatch: Read outbox YAML, post to platforms, write results. + * Continue on failure — report per-action results. + */ + +import { readFileSync, existsSync, mkdirSync, renameSync } from "node:fs" +import { resolve, join } from "node:path" +import { parse, stringify } from "yaml" +import { getPlatformAsync } from "../platforms/index.js" +import { validateOutbox, type OutboxFile, type OutboxAction } from "./validate.js" +import { writeFileAtomic } from "../util/fs.js" + +interface DispatchResult { + action: string + platform: string + status: "ok" | "error" + id?: string + targetId?: string + inboxIdsRemoved?: string[] + archivedOutbox?: string + error?: string +} + +export async function dispatch(opts: { + file?: string + dryRun?: boolean +}): Promise { + const filePath = resolve(process.cwd(), opts.file ?? "outbox.yaml") + + if (!existsSync(filePath)) { + console.error(`File not found: ${filePath}`) + process.exit(1) + } + + let outbox: OutboxFile + try { + outbox = parse(readFileSync(filePath, "utf-8")) as OutboxFile + } catch (err) { + console.error(`Failed to parse ${filePath}: ${err instanceof Error ? err.message : err}`) + process.exit(1) + } + + // Validate + const validation = validateOutbox(outbox) + if (validation.warnings.length > 0) { + for (const w of validation.warnings) console.error(`Warning: ${w}`) + } + if (!validation.valid) { + for (const e of validation.errors) console.error(`Error: ${e}`) + console.error(`Validation failed: ${validation.errors.length} error(s)`) + process.exit(1) + } + + if (opts.dryRun) { + console.log("Dry run: validation passed.") + process.exit(0) + } + + // Load inbox for validation and later pruning + const inboxPath = resolve(process.cwd(), "inbox.yaml") + let inboxNotifications: Array<{ id: string; postId?: string }> = [] + if (existsSync(inboxPath)) { + try { + const inbox = parse(readFileSync(inboxPath, "utf-8")) as { notifications?: Array<{ id: string; postId?: string }> } + inboxNotifications = inbox?.notifications ?? [] + } catch { + // Best effort only + } + } + + // Load persistent processed state + const processedPath = resolve(process.cwd(), "processed.yaml") + let persistentProcessed: Set = new Set() + if (existsSync(processedPath)) { + try { + const processedData = parse(readFileSync(processedPath, "utf-8")) as { processed?: string[] } + if (processedData?.processed) { + persistentProcessed = new Set(processedData.processed) + } + } catch { + // Best effort only + } + } + + // Merge persistent + outbox-provided processed IDs + const allProcessed: Set = new Set(persistentProcessed) + if (outbox.processed) { + for (const id of outbox.processed) allProcessed.add(id) + } + + // Filter inbox: remove everything in the processed set before dispatch + if (allProcessed.size > 0 && inboxNotifications.length > 0) { + const before = inboxNotifications.length + inboxNotifications = inboxNotifications.filter( + (n) => !allProcessed.has(n.id) && !allProcessed.has(n.postId ?? ""), + ) + const filtered = before - inboxNotifications.length + if (filtered > 0) { + console.log(`Filtered ${filtered} previously-processed notification(s) from inbox`) + } + } + + // Dispatch + const results: DispatchResult[] = [] + const processedNotifIds: string[] = [] + + for (let i = 0; i < outbox.dispatch.length; i++) { + const action = outbox.dispatch[i] + + if (action.ignore) { + processedNotifIds.push(action.ignore.id) + console.log(`Ignoring ${action.ignore.id} (${action.ignore.reason ?? "unspecified"})`) + continue + } + + if (action.reply) { + const r = action.reply + try { + const platform = await getPlatformAsync(r.platform) + const res = await platform.reply(r.id, r.text) + results.push({ action: "reply", platform: r.platform, status: "ok", id: res.id, targetId: r.id }) + // Only prune from inbox if the target came from there + const targetExistsInInbox = inboxNotifications.some((n) => n.id === r.id || n.postId === r.id) + if (targetExistsInInbox) processedNotifIds.push(r.id) + console.log(`Replied on ${r.platform}: ${res.id}`) + } catch (err) { + const msg = err instanceof Error ? err.message : String(err) + results.push({ action: "reply", platform: r.platform, status: "error", targetId: r.id, error: msg }) + console.error(`Reply failed on ${r.platform}: ${msg}`) + } + } + + if (action.post) { + const p = action.post + // Determine platforms and text + const targets: { platform: string; text: string }[] = [] + + if (p.platforms && typeof p.platforms === "object" && !Array.isArray(p.platforms)) { + // Per-platform text + for (const [plat, text] of Object.entries(p.platforms)) { + targets.push({ platform: plat, text }) + } + } else if (p.text && p.platforms && Array.isArray(p.platforms)) { + // Same text, multiple platforms + for (const plat of p.platforms) { + targets.push({ platform: plat, text: p.text }) + } + } else if (p.text) { + // Single platform not specified — default to bsky + targets.push({ platform: "bsky", text: p.text }) + } + + for (const t of targets) { + try { + const platform = await getPlatformAsync(t.platform) + const res = await platform.post(t.text) + results.push({ action: "post", platform: t.platform, status: "ok", id: res.id }) + console.log(`Posted on ${t.platform}: ${res.id}`) + } catch (err) { + const msg = err instanceof Error ? err.message : String(err) + results.push({ action: "post", platform: t.platform, status: "error", error: msg }) + console.error(`Post failed on ${t.platform}: ${msg}`) + } + } + } + + if (action.thread) { + const t = action.thread + try { + const platform = await getPlatformAsync(t.platform) + const res = await platform.thread(t.posts) + for (const r of res) { + results.push({ action: "thread", platform: t.platform, status: "ok", id: r.id }) + } + console.log(`Thread posted on ${t.platform}: ${res.length} posts`) + } catch (err) { + const msg = err instanceof Error ? err.message : String(err) + // Find how many posts succeeded before failure + const okCount = results.filter( + (r) => r.action === "thread" && r.platform === t.platform && r.status === "ok", + ).length + const lastOk = okCount > 0 ? results.filter( + (r) => r.action === "thread" && r.platform === t.platform && r.status === "ok", + ).pop() : undefined + results.push({ + action: "thread", + platform: t.platform, + status: "error", + error: msg, + resumeFrom: okCount > 0 ? { + index: okCount, + parentId: lastOk?.id, + remainingPosts: t.posts.slice(okCount), + } : undefined, + } as any) + console.error(`Thread failed on ${t.platform} at post ${okCount + 1}/${t.posts.length}: ${msg}`) + } + } + + if (action.annotate) { + const a = action.annotate + try { + const platform = await getPlatformAsync(a.platform) + if (!platform.annotate) { + throw new Error(`Platform ${a.platform} does not support annotations`) + } + const res = await platform.annotate(a.id, a.text, { motivation: a.motivation }) + results.push({ action: "annotate", platform: a.platform, status: "ok", id: res.id }) + console.log(`Annotated on ${a.platform}: ${res.id}`) + } catch (err) { + const msg = err instanceof Error ? err.message : String(err) + results.push({ action: "annotate", platform: a.platform, status: "error", error: msg }) + console.error(`Annotate failed on ${a.platform}: ${msg}`) + } + } + } + + // Archive outbox + const archiveDir = resolve(process.cwd(), "outbox_archive") + mkdirSync(archiveDir, { recursive: true }) + const timestamp = new Date().toISOString().replace(/[:.]/g, "-") + const archivedOutbox = join(archiveDir, `${timestamp}_outbox.yaml`) + renameSync(filePath, archivedOutbox) + + // Remove processed items from inbox + let inboxIdsRemoved: string[] = [] + if (processedNotifIds.length > 0 && existsSync(inboxPath)) { + try { + const inbox = parse(readFileSync(inboxPath, "utf-8")) as { notifications: any[]; _sync?: Record } + if (inbox?.notifications) { + const processedSet = new Set(processedNotifIds) + const before = inbox.notifications + const remaining = before.filter((n: any) => !processedSet.has(n.id) && !processedSet.has(n.postId)) + inboxIdsRemoved = before + .filter((n: any) => processedSet.has(n.id) || processedSet.has(n.postId)) + .map((n: any) => n.id) + inbox.notifications = remaining + if (inbox._sync) { + inbox._sync = { + ...inbox._sync, + totalCount: remaining.length, + } + } + writeFileAtomic(inboxPath, stringify(inbox, { lineWidth: 120 })) + } + } catch { + // Best effort + } + } + + for (const result of results) { + if (result.status === "ok") { + result.archivedOutbox = archivedOutbox + if (result.targetId) { + result.inboxIdsRemoved = inboxIdsRemoved.filter((id) => id === result.targetId) + } + } + } + + // Write results + const resultPath = resolve(process.cwd(), "dispatch_result.yaml") + writeFileAtomic(resultPath, stringify({ results, archivedOutbox, inboxIdsRemoved }, { lineWidth: 120 })) + + // Persist processed set for future cycles + const newProcessed = new Set([...persistentProcessed, ...processedNotifIds]) + writeFileAtomic( + processedPath, + stringify({ processed: [...newProcessed] }, { lineWidth: 120 }), + ) + + const ok = results.filter((r) => r.status === "ok").length + const failed = results.filter((r) => r.status === "error").length + console.log(`\nDispatch complete: ${ok} ok, ${failed} failed`) + + if (failed > 0) process.exit(2) // Partial failure +} diff --git a/social-cli/src/commands/feed.ts b/social-cli/src/commands/feed.ts new file mode 100644 index 0000000..8d7a2d3 --- /dev/null +++ b/social-cli/src/commands/feed.ts @@ -0,0 +1,27 @@ +/** + * feed: Fetch timeline from a platform → feed.yaml + */ + +import { resolve } from "node:path" +import { stringify } from "yaml" +import { getPlatformAsync } from "../platforms/index.js" +import { writeFileAtomic } from "../util/fs.js" + +export async function feed(opts: { + platform?: string + limit?: number + output?: string +}): Promise { + const platform = await getPlatformAsync(opts.platform ?? "bsky") + const items = await platform.feed(opts.limit ?? 50) + const output = opts.output ?? "feed.yaml" + const content = stringify(items, { lineWidth: 120 }) + + if (output === "-") { + process.stdout.write(content) + } else { + const outputPath = resolve(process.cwd(), output) + writeFileAtomic(outputPath, content) + console.log(`Fetched ${items.length} posts → ${outputPath}`) + } +} diff --git a/social-cli/src/commands/search.ts b/social-cli/src/commands/search.ts new file mode 100644 index 0000000..1f1d844 --- /dev/null +++ b/social-cli/src/commands/search.ts @@ -0,0 +1,15 @@ +/** + * search: Search posts across platforms. + */ + +import { stringify } from "yaml" +import { getPlatformAsync } from "../platforms/index.js" + +export async function search(query: string, opts: { + platform?: string + limit?: number +}): Promise { + const platform = await getPlatformAsync(opts.platform ?? "bsky") + const results = await platform.search(query, opts.limit ?? 10) + process.stdout.write(stringify(results, { lineWidth: 120 })) +} diff --git a/social-cli/src/commands/sync.ts b/social-cli/src/commands/sync.ts new file mode 100644 index 0000000..d113212 --- /dev/null +++ b/social-cli/src/commands/sync.ts @@ -0,0 +1,223 @@ +/** + * sync: Fetch notifications from all configured platforms → inbox.yaml + */ + +import { readFileSync, existsSync, readdirSync } from "node:fs" +import { resolve, join } from "node:path" +import { stringify, parse } from "yaml" +import { getPlatformAsync, availablePlatforms } from "../platforms/index.js" +import type { Notification } from "../platforms/types.js" +import { writeFileAtomic } from "../util/fs.js" + +interface InboxFile { + notifications: Notification[] + _sync: { + timestamp: string + platforms: string[] + unreadOnly: boolean + usersDir?: string + usersMatched?: number + newCount: number + totalCount: number + dropped?: number + /** Per-platform cursors for incremental sync. */ + cursors?: Record + } +} + +/** + * Build a lookup map from a users directory. + * Supports two layouts: + * users/{handle}.md (flat, platform-agnostic) + * users/{platform}/{handle}.md (nested, platform-specific) + * + * Returns a nested map: platform → handle → filepath + * The "_flat" key holds platform-agnostic entries. + */ +function buildUserIndex(usersDir: string): Map> { + const index = new Map>() + index.set("_flat", new Map()) + if (!existsSync(usersDir)) return index + + const entries = readdirSync(usersDir, { withFileTypes: true }) + for (const entry of entries) { + if (entry.isFile() && entry.name.endsWith(".md") && !entry.name.startsWith("_")) { + // Flat: users/cameron.md + const name = entry.name.replace(/\.md$/, "").toLowerCase() + index.get("_flat")!.set(name, join(usersDir, entry.name)) + } else if (entry.isDirectory() && !entry.name.startsWith("_")) { + // Nested: users/bsky/*.md, users/x/*.md + const platformDir = join(usersDir, entry.name) + const platformMap = new Map() + const files = readdirSync(platformDir).filter((f) => f.endsWith(".md") && !f.startsWith("_")) + for (const file of files) { + const name = file.replace(/\.md$/, "").toLowerCase() + platformMap.set(name, join(platformDir, file)) + } + index.set(entry.name.toLowerCase(), platformMap) + } + } + return index +} + +/** Platform name → directory name mapping. */ +const PLATFORM_DIR_ALIASES: Record = { + bsky: ["bsky", "bluesky", "atproto"], + x: ["x", "twitter"], +} + +/** Exact lookup by a single key against platform-specific then flat dirs. */ +function exactLookup( + key: string, + platform: string, + index: Map>, +): string | null { + const lower = key.toLowerCase() + + // Check platform-specific directories first + const dirNames = PLATFORM_DIR_ALIASES[platform] ?? [platform] + for (const dir of dirNames) { + const platformMap = index.get(dir) + if (platformMap?.has(lower)) return platformMap.get(lower)! + } + + // Fall back to flat directory + const flat = index.get("_flat")! + if (flat.has(lower)) return flat.get(lower)! + + return null +} + +/** + * Try to find a user file for a notification author. + * Tries permanent ID (DID/user ID) first, then handle. + */ +function lookupUser( + handle: string, + authorId: string | undefined, + platform: string, + index: Map>, +): string | null { + // Permanent ID takes priority + if (authorId) { + const found = exactLookup(authorId, platform, index) + if (found) return found + } + // Fall back to handle + return exactLookup(handle, platform, index) +} + +export async function sync(opts: { + platforms?: string[] + unreadOnly?: boolean + limit?: number + output?: string + maxItems?: number + usersDir?: string + /** Clear cursors and re-fetch all notifications from scratch. */ + reset?: boolean + /** Clear both cursors and the local inbox for a fully fresh start. */ + clear?: boolean +}): Promise { + const outputPath = resolve(process.cwd(), opts.output ?? "inbox.yaml") + const targetPlatforms = opts.platforms ?? availablePlatforms() + + // --clear: wipe everything and start from scratch + let existing: Notification[] = [] + const existingIds = new Set() + let cursors: Record = {} + + if (!opts.clear && existsSync(outputPath)) { + try { + const raw = parse(readFileSync(outputPath, "utf-8")) as InboxFile + // Load cursors only if not resetting + if (!opts.reset && raw?._sync?.cursors) cursors = raw._sync.cursors + // Load existing items only if not clearing + existing = raw?.notifications ?? [] + for (const n of existing) existingIds.add(n.id) + } catch { + // Corrupt file, start fresh + } + } + + const allNotifs = [...existing] + let newCount = 0 + + for (const name of targetPlatforms) { + try { + const platform = await getPlatformAsync(name) + // Fetch without passing a cursor — we filter by timestamp instead. + // Disable unreadOnly on --clear so we get the full recent history as baseline. + const result = await platform.notifications({ + limit: opts.limit ?? 50, + unreadOnly: opts.clear ? false : (opts.unreadOnly ?? true), + }) + + const cutoff = cursors[name] ? new Date(cursors[name]).getTime() : 0 + + for (const n of result.notifications) { + const itemTime = new Date(n.timestamp).getTime() + // Skip items older than the cutoff + if (cutoff > 0 && itemTime <= cutoff) continue + if (!existingIds.has(n.id)) { + allNotifs.push(n) + existingIds.add(n.id) + newCount++ + } + } + + // Track the newest timestamp seen as the cursor for next sync. + // result.notifications are sorted newest-first, so the first item is newest. + if (result.notifications.length > 0) { + cursors[name] = result.notifications[0].timestamp + } + } catch (err) { + const msg = err instanceof Error ? err.message : String(err) + console.error(`[${name}] sync failed: ${msg}`) + // Continue on failure + } + } + + // Cap inbox size to prevent unbounded growth + const maxItems = opts.maxItems ?? 200 + const capped = allNotifs.slice(-maxItems) + const dropped = allNotifs.length - capped.length + + // Enrich with user context if --users-dir provided + let usersMatched = 0 + if (opts.usersDir) { + const userIndex = buildUserIndex(opts.usersDir) + for (const notif of capped as Array) { + if (!notif.author) continue + const filePath = lookupUser(notif.author, notif.authorId, notif.platform, userIndex) + if (filePath) { + try { + notif.userContext = readFileSync(filePath, "utf-8") + usersMatched++ + } catch { + // Skip unreadable files + } + } + } + } + + const inbox: InboxFile = { + notifications: capped, + _sync: { + timestamp: new Date().toISOString(), + platforms: targetPlatforms, + unreadOnly: opts.unreadOnly ?? true, + newCount, + totalCount: capped.length, + cursors, + ...(dropped > 0 ? { dropped } : {}), + ...(opts.usersDir ? { usersDir: opts.usersDir, usersMatched } : {}), + }, + } + + writeFileAtomic(outputPath, stringify(inbox, { lineWidth: 120 })) + let msg = `Synced ${newCount} new notifications (${capped.length} pending total) → ${outputPath}` + if (dropped > 0) msg += ` (${dropped} oldest dropped, cap: ${maxItems})` + if (opts.usersDir) msg += ` (${usersMatched} users matched)` + console.log(msg) +} diff --git a/social-cli/src/commands/validate.ts b/social-cli/src/commands/validate.ts new file mode 100644 index 0000000..6efb0c8 --- /dev/null +++ b/social-cli/src/commands/validate.ts @@ -0,0 +1,170 @@ +/** + * validate: Pre-flight validation of outbox YAML. + * Used by dispatch --dry-run and as standalone. + */ + +import { PLATFORM_LIMITS } from "../platforms/types.js" + +export interface OutboxAction { + reply?: { + platform: string + id: string + text: string + } + post?: { + text?: string + platforms?: string[] | Record + } + thread?: { + platform: string + posts: string[] + } + annotate?: { + platform: string + id: string + text: string + motivation?: string + } + ignore?: { + id: string + reason: string + } +} + +export interface OutboxFile { + dispatch: OutboxAction[] + /** Persistently ignore these notification IDs across all future cycles. */ + processed?: string[] +} + +export interface ValidationResult { + valid: boolean + errors: string[] + warnings: string[] +} + +export function validateOutbox(outbox: OutboxFile): ValidationResult { + const errors: string[] = [] + const warnings: string[] = [] + + if (!outbox?.dispatch) { + errors.push("Missing 'dispatch' key") + return { valid: false, errors, warnings } + } + + if (!Array.isArray(outbox.dispatch)) { + errors.push("'dispatch' must be an array") + return { valid: false, errors, warnings } + } + + if (outbox.dispatch.length === 0) { + errors.push("Empty dispatch list") + return { valid: false, errors, warnings } + } + + for (let i = 0; i < outbox.dispatch.length; i++) { + const action = outbox.dispatch[i] + const prefix = `Action ${i}` + + // Determine action type + const types = ["reply", "post", "thread", "annotate", "ignore"].filter( + (t) => action[t as keyof OutboxAction] !== undefined, + ) + + if (types.length === 0) { + errors.push(`${prefix}: No recognized action type (reply, post, thread, annotate, ignore)`) + continue + } + if (types.length > 1) { + errors.push(`${prefix}: Multiple action types in one entry: ${types.join(", ")}`) + continue + } + + const type = types[0] + + if (type === "reply") { + const r = action.reply! + if (!r.platform) errors.push(`${prefix}: reply missing 'platform'`) + if (!r.id) errors.push(`${prefix}: reply missing 'id'`) + if (!r.text) errors.push(`${prefix}: reply missing 'text'`) + if (r.platform && r.text) { + const limit = PLATFORM_LIMITS[r.platform] + if (limit && r.text.length > limit.chars) { + errors.push(`${prefix}: reply text exceeds ${r.platform} limit (${r.text.length}/${limit.chars})`) + } + } + } + + if (type === "post") { + const p = action.post! + if (!p.text && !p.platforms) { + errors.push(`${prefix}: post needs 'text' or 'platforms' with per-platform text`) + } + // Validate char limits for each platform + if (p.text && p.platforms && Array.isArray(p.platforms)) { + for (const plat of p.platforms) { + const limit = PLATFORM_LIMITS[plat] + if (limit && p.text.length > limit.chars) { + errors.push(`${prefix}: post text exceeds ${plat} limit (${p.text.length}/${limit.chars})`) + } + } + } + if (p.platforms && typeof p.platforms === "object" && !Array.isArray(p.platforms)) { + for (const [plat, text] of Object.entries(p.platforms)) { + const limit = PLATFORM_LIMITS[plat] + if (limit && text.length > limit.chars) { + errors.push(`${prefix}: post text for ${plat} exceeds limit (${text.length}/${limit.chars})`) + } + } + } + } + + if (type === "thread") { + const t = action.thread! + if (!t.platform) errors.push(`${prefix}: thread missing 'platform'`) + if (!t.posts || !Array.isArray(t.posts) || t.posts.length === 0) { + errors.push(`${prefix}: thread needs non-empty 'posts' array`) + } + if (t.platform && t.posts) { + const limit = PLATFORM_LIMITS[t.platform] + if (limit) { + for (let j = 0; j < t.posts.length; j++) { + if (t.posts[j].length > limit.chars) { + errors.push(`${prefix}: thread post ${j} exceeds ${t.platform} limit (${t.posts[j].length}/${limit.chars})`) + } + } + } + } + } + + if (type === "annotate") { + const a = action.annotate! + if (!a.platform) errors.push(`${prefix}: annotate missing 'platform'`) + if (!a.id) errors.push(`${prefix}: annotate missing 'id'`) + if (!a.text) errors.push(`${prefix}: annotate missing 'text'`) + if (a.platform && a.platform !== "bsky") { + warnings.push(`${prefix}: annotations only supported on bsky`) + } + } + + if (type === "ignore") { + const ig = action.ignore! + if (!ig.id) errors.push(`${prefix}: ignore missing 'id'`) + if (!ig.reason) warnings.push(`${prefix}: ignore missing 'reason'`) + } + } + + if (outbox.processed) { + if (!Array.isArray(outbox.processed)) { + errors.push("'processed' must be an array of notification IDs") + } else if (outbox.processed.length > 0 && outbox.dispatch.length === 0) { + warnings.push("'processed' is non-empty but 'dispatch' is empty — all items will be auto-ignored") + } + } + + return { + valid: errors.length === 0, + errors, + warnings, + } +} diff --git a/social-cli/src/config.ts b/social-cli/src/config.ts new file mode 100644 index 0000000..44ee5e6 --- /dev/null +++ b/social-cli/src/config.ts @@ -0,0 +1,62 @@ +/** + * Configuration loading for social-cli. + * Reads config.yaml and resolves credentials from .env files. + */ + +import { readFileSync, existsSync } from "node:fs" +import { resolve, dirname } from "node:path" +import { parse } from "yaml" +import { config as loadDotenv } from "dotenv" + +export interface AccountConfig { + handle: string + pds?: string // ATProto PDS URL (Bluesky only) + credentials?: string // Path to .env file +} + +export interface Config { + accounts: Record +} + +const CONFIG_PATHS = [ + resolve(process.cwd(), "config.yaml"), + resolve(process.env.HOME ?? "~", ".config/social-cli/config.yaml"), +] + +export function loadConfig(): Config { + for (const p of CONFIG_PATHS) { + if (existsSync(p)) { + const raw = readFileSync(p, "utf-8") + return parse(raw) as Config + } + } + + // Fall back to env-only config (no config.yaml) + return { accounts: {} } +} + +/** + * Load credentials for a specific platform. + * Checks the config's credentials path, then falls back to .env in cwd. + */ +export function loadCredentials(platform: string, config: Config): Record { + const account = config.accounts[platform] + const envPaths: string[] = [] + + if (account?.credentials) { + // Resolve relative to cwd + envPaths.push(resolve(process.cwd(), account.credentials)) + } + + // Always check cwd .env as fallback + envPaths.push(resolve(process.cwd(), ".env")) + + for (const p of envPaths) { + if (existsSync(p)) { + loadDotenv({ path: p, override: true, quiet: true }) + break + } + } + + return process.env as Record +} diff --git a/social-cli/src/platforms/bluesky.ts b/social-cli/src/platforms/bluesky.ts new file mode 100644 index 0000000..3fb331a --- /dev/null +++ b/social-cli/src/platforms/bluesky.ts @@ -0,0 +1,369 @@ +/** + * Bluesky (ATProto) platform implementation. + * Uses @atproto/api — the official reference SDK. + */ + +import { Agent, CredentialSession, RichText, AppBskyFeedPost, ComAtprotoRepoStrongRef } from "@atproto/api" +import { createHash } from "node:crypto" +import type { + SocialPlatform, + PostOpts, + PostResult, + Notification, + NotifOpts, + SearchResult, + FeedItem, + RateLimitInfo, + AnnotateOpts, + ProfileInfo, +} from "./types.js" +import { loadConfig, loadCredentials } from "../config.js" +import { withRetry } from "../util/retry.js" + +let _agent: Agent | null = null +let _session: CredentialSession | null = null +let _credentials: { handle: string; password: string; pds: string } | null = null + +function loadBskyCredentials(): { handle: string; password: string; pds: string } { + if (_credentials) return _credentials + const config = loadConfig() + loadCredentials("bsky", config) + + const handle = process.env.ATPROTO_HANDLE ?? process.env.BSKY_USERNAME ?? config.accounts.bsky?.handle + const password = process.env.ATPROTO_APP_PASSWORD ?? process.env.BSKY_PASSWORD + const pds = process.env.ATPROTO_PDS ?? process.env.PDS_URI ?? config.accounts.bsky?.pds ?? "https://bsky.social" + + if (!handle || !password) { + throw new Error("ATPROTO_HANDLE and ATPROTO_APP_PASSWORD (or BSKY_USERNAME/BSKY_PASSWORD) required") + } + + _credentials = { handle, password, pds } + return _credentials +} + +async function getAgent(): Promise { + const creds = loadBskyCredentials() + + if (_agent && _session) { + // Check if session is still active. CredentialSession tracks this. + if (_session.hasSession) return _agent + // Session expired — try refresh, fall back to re-login + try { + await _session.refreshSession() + return _agent! + } catch { + _agent = null + _session = null + } + } + + _session = new CredentialSession(new URL(creds.pds)) + await _session.login({ identifier: creds.handle, password: creds.password }) + _agent = new Agent(_session) + return _agent +} + +/** Wrap an API call with session recovery + retry. */ +async function withSession(fn: (agent: Agent) => Promise): Promise { + return withRetry(async () => { + try { + const agent = await getAgent() + return await fn(agent) + } catch (err: any) { + // Session expired mid-request — force re-auth on next attempt + if (err?.status === 401 || err?.error === "ExpiredToken") { + _agent = null + _session = null + } + throw err + } + }) +} + +export const bluesky: SocialPlatform = { + name: "bsky", + + async post(text: string, opts?: PostOpts): Promise { + return withSession(async (agent) => { + const rt = new RichText({ text }) + await rt.detectFacets(agent) + + let embed: any = undefined + if (opts?.quoteId) { + const quoted = (await agent.app.bsky.feed.getPosts({ uris: [opts.quoteId] })).data.posts[0] + if (quoted) { + embed = { + $type: "app.bsky.embed.record", + record: { cid: quoted.cid, uri: quoted.uri }, + } + } + } + + const res = await agent.post({ text: rt.text, facets: rt.facets, embed }) + return { platform: "bsky", id: res.uri, uri: res.uri, text } + }) + }, + + async reply(targetId: string, text: string, opts?: PostOpts): Promise { + return withSession(async (agent) => { + const rt = new RichText({ text }) + await rt.detectFacets(agent) + + // Fetch parent post to build reply ref + const parent = (await agent.app.bsky.feed.getPosts({ uris: [targetId] })).data.posts[0] + if (!parent) throw new Error(`Post not found: ${targetId}`) + + const parentRef = { cid: parent.cid, uri: parent.uri } + const record = parent.record as AppBskyFeedPost.Record + const rootRef = record.reply?.root ?? parentRef + + const res = await agent.post({ + text: rt.text, + facets: rt.facets, + reply: { parent: parentRef, root: rootRef }, + }) + + return { platform: "bsky", id: res.uri, uri: res.uri, text } + }) + }, + + async thread(posts: string[]): Promise { + // Thread uses withSession for initial auth, but individual posts + // are retried individually to avoid re-posting successful ones. + const agent = await getAgent() + const results: PostResult[] = [] + let parentRef: ComAtprotoRepoStrongRef.Main | null = null + let rootRef: ComAtprotoRepoStrongRef.Main | null = null + + for (const text of posts) { + const rt = new RichText({ text }) + await rt.detectFacets(agent) + + const postData: any = { text: rt.text, facets: rt.facets } + if (parentRef && rootRef) { + postData.reply = { parent: parentRef, root: rootRef } + } + + const res = await withRetry(() => agent.post(postData)) + const ref = { cid: res.cid, uri: res.uri } + + if (!rootRef) rootRef = ref + parentRef = ref + + results.push({ platform: "bsky", id: res.uri, uri: res.uri, text }) + } + + return results + }, + + async notifications(opts?: NotifOpts): Promise<{ notifications: Notification[]; cursor?: string }> { + return withSession(async (agent) => { + const limit = opts?.limit ?? 50 + const params: Record = { limit } + if (opts?.cursor) params.cursor = opts.cursor + const res = await agent.app.bsky.notification.listNotifications(params) + + const notifs: Notification[] = [] + for (const n of res.data.notifications) { + // Skip passive engagement + if (n.reason === "like" || n.reason === "repost") continue + if (opts?.unreadOnly && n.isRead) continue + + const record = n.record as any + const item: Notification = { + id: n.uri, + platform: "bsky", + type: n.reason, + author: n.author.handle, + authorId: n.author.did, + postId: n.uri, + text: record?.text ?? "", + timestamp: n.indexedAt, + } + + // Fetch thread context for replies/quotes/mentions + if (["reply", "quote", "mention"].includes(n.reason)) { + try { + const threadRes = await agent.app.bsky.feed.getPostThread({ + uri: n.uri, + depth: 0, + parentHeight: 5, + }) + const context: { author: string; text: string }[] = [] + let curr = threadRes.data.thread as any + while (curr?.parent) { + curr = curr.parent + if (curr?.post?.record?.text) { + context.unshift({ + author: curr.post.author.handle, + text: curr.post.record.text, + }) + } + } + if (context.length > 0) item.threadContext = context + } catch { + // Thread context is best-effort + } + } + + notifs.push(item) + } + + return { notifications: notifs, cursor: res.data.cursor } + }) + }, + + async search(query: string, limit = 10): Promise { + return withSession(async (agent) => { + const res = await agent.app.bsky.feed.searchPosts({ q: query, limit }) + return res.data.posts.map((p) => ({ + platform: "bsky", + id: p.uri, + author: p.author.handle, + text: (p.record as any).text ?? "", + timestamp: p.indexedAt, + })) + }) + }, + + async feed(limit = 50): Promise { + return withSession(async (agent) => { + const res = await agent.getTimeline({ limit }) + return res.data.feed.map((item) => ({ + platform: "bsky", + id: item.post.uri, + author: item.post.author.handle, + text: (item.post.record as any).text ?? "", + timestamp: item.post.indexedAt, + likeCount: item.post.likeCount ?? 0, + replyCount: item.post.replyCount ?? 0, + repostCount: item.post.repostCount ?? 0, + })) + }) + }, + + async rateLimitStatus(): Promise { + // ATProto doesn't expose rate limits the same way. + // Return a generous default. Real limits are per-PDS. + return { + platform: "bsky", + remaining: 100, + limit: 100, + resetsAt: new Date(Date.now() + 5 * 60 * 1000).toISOString(), + } + }, + + async annotate(targetId: string, text: string, opts?: AnnotateOpts): Promise { + return withSession(async (agent) => { + // Convert AT-URI to bsky.app HTTP URL for Margin compatibility + let targetUri = targetId + if (targetUri.startsWith("at://")) { + const parts = targetUri.replace("at://", "").split("/") + if (parts.length >= 3) { + targetUri = `https://bsky.app/profile/${parts[0]}/post/${parts[2]}` + } + } + + const sourceHash = createHash("sha256").update(targetUri).digest("hex") + const record: Record = { + $type: "at.margin.annotation", + createdAt: new Date().toISOString(), + target: { + source: targetUri, + sourceHash, + }, + body: { value: text }, + motivation: opts?.motivation ?? "commenting", + } + + if (opts?.quote) { + record.target.selector = { + type: "TextQuoteSelector", + exact: opts.quote, + } + } + + const session = (agent as any).session ?? (agent as any)._session + const did = agent.did ?? session?.did + if (!did) throw new Error("Cannot determine DID for annotation") + + const res = await agent.com.atproto.repo.createRecord({ + repo: did, + collection: "at.margin.annotation", + record, + }) + + return { platform: "bsky", id: res.data.uri, uri: res.data.uri, text } + }) + }, + + async delete(targetId: string): Promise { + return withSession(async (agent) => { + // AT-URI format: at://did/collection/rkey + if (!targetId.startsWith("at://")) { + throw new Error("Bluesky delete requires an AT-URI (at://...)") + } + const parts = targetId.replace("at://", "").split("/") + if (parts.length < 3) throw new Error(`Invalid AT-URI: ${targetId}`) + const [repo, collection, rkey] = parts + await agent.com.atproto.repo.deleteRecord({ repo, collection, rkey }) + }) + }, + + async like(targetId: string): Promise { + return withSession(async (agent) => { + const posts = await agent.app.bsky.feed.getPosts({ uris: [targetId] }) + const post = posts.data.posts[0] + if (!post) throw new Error(`Post not found: ${targetId}`) + await agent.like(post.uri, post.cid) + }) + }, + + async whoami(): Promise { + return withSession(async (agent) => { + const profile = await agent.app.bsky.actor.getProfile({ actor: agent.did! }) + return { + platform: "bsky", + handle: profile.data.handle, + displayName: profile.data.displayName, + bio: profile.data.description, + did: profile.data.did, + followersCount: profile.data.followersCount, + followingCount: profile.data.followsCount, + postsCount: profile.data.postsCount, + } + }) + }, + + async userPosts(handle: string, limit = 20): Promise { + return withSession(async (agent) => { + const res = await agent.app.bsky.feed.getAuthorFeed({ actor: handle, limit }) + return res.data.feed.map((item) => ({ + platform: "bsky", + id: item.post.uri, + author: item.post.author.handle, + text: (item.post.record as any).text ?? "", + timestamp: item.post.indexedAt, + likeCount: item.post.likeCount ?? 0, + replyCount: item.post.replyCount ?? 0, + repostCount: item.post.repostCount ?? 0, + })) + }) + }, + + async profile(handle: string): Promise { + return withSession(async (agent) => { + const profile = await agent.app.bsky.actor.getProfile({ actor: handle }) + return { + platform: "bsky", + handle: profile.data.handle, + displayName: profile.data.displayName, + bio: profile.data.description, + did: profile.data.did, + followersCount: profile.data.followersCount, + followingCount: profile.data.followsCount, + postsCount: profile.data.postsCount, + } + }) + }, +} diff --git a/social-cli/src/platforms/index.ts b/social-cli/src/platforms/index.ts new file mode 100644 index 0000000..2ab4f0c --- /dev/null +++ b/social-cli/src/platforms/index.ts @@ -0,0 +1,47 @@ +/** + * Platform registry. + * Lazily initializes platforms only when first accessed. + */ + +import type { SocialPlatform } from "./types.js" + +const registry: Record SocialPlatform> = { + bsky: () => { + const { bluesky } = require("./bluesky.js") as typeof import("./bluesky.js") + return bluesky + }, + x: () => { + const { x } = require("./x.js") as typeof import("./x.js") + return x + }, +} + +const loaded: Record = {} + +export function getPlatform(name: string): SocialPlatform { + if (loaded[name]) return loaded[name] + const factory = registry[name] + if (!factory) throw new Error(`Unknown platform: ${name}. Available: ${Object.keys(registry).join(", ")}`) + loaded[name] = factory() + return loaded[name] +} + +export function availablePlatforms(): string[] { + return Object.keys(registry) +} + +export async function getPlatformAsync(name: string): Promise { + if (loaded[name]) return loaded[name] + + if (name === "bsky") { + const mod = await import("./bluesky.js") + loaded[name] = mod.bluesky + } else if (name === "x") { + const mod = await import("./x.js") + loaded[name] = mod.x + } else { + throw new Error(`Unknown platform: ${name}. Available: ${Object.keys(registry).join(", ")}`) + } + + return loaded[name] +} diff --git a/social-cli/src/platforms/types.ts b/social-cli/src/platforms/types.ts new file mode 100644 index 0000000..09366f5 --- /dev/null +++ b/social-cli/src/platforms/types.ts @@ -0,0 +1,119 @@ +/** + * Platform abstraction layer for social-cli. + * Each platform implements this interface. + */ + +export interface PostOpts { + /** Quote/repost a target post. */ + quoteId?: string + /** Media attachment paths. */ + media?: string[] +} + +export interface PostResult { + platform: string + id: string + uri?: string + text: string +} + +export interface AnnotateOpts { + /** W3C motivation: commenting, highlighting, describing. */ + motivation?: string + /** Exact text to anchor the annotation to. */ + quote?: string +} + +export interface Notification { + id: string + platform: string + type: string // reply, mention, quote, follow, like + author: string + authorId?: string // permanent ID (DID for Bluesky, user ID for X) + postId: string + text: string + timestamp: string + threadContext?: { author: string; text: string }[] + userContext?: string +} + +export interface SearchResult { + platform: string + id: string + author: string + text: string + timestamp: string +} + +export interface FeedItem { + platform: string + id: string + author: string + text: string + timestamp: string + likeCount?: number + replyCount?: number + repostCount?: number +} + +export interface RateLimitInfo { + platform: string + remaining: number + limit: number + resetsAt: string +} + +export interface NotifResult { + notifications: Notification[] + /** Opaque cursor to pass back on next call to fetch subsequent pages / newer items. */ + cursor?: string +} + +export interface NotifOpts { + limit?: number + /** Only fetch unread. */ + unreadOnly?: boolean + /** Cursor from a previous NotifResult — resumes from that point. */ + cursor?: string +} + +export interface ProfileInfo { + platform: string + handle: string + displayName?: string + bio?: string + did?: string // ATProto DID + followersCount?: number + followingCount?: number + postsCount?: number +} + +export interface SocialPlatform { + name: string + post(text: string, opts?: PostOpts): Promise + reply(targetId: string, text: string, opts?: PostOpts): Promise + thread(posts: string[]): Promise + notifications(opts?: NotifOpts): Promise + search(query: string, limit?: number): Promise + feed(limit?: number): Promise + rateLimitStatus(): Promise + + /** Delete a post by ID/URI. */ + delete?(targetId: string): Promise + /** Like a post by ID/URI. */ + like?(targetId: string): Promise + /** Get current account info. */ + whoami?(): Promise + /** Look up a user by handle. */ + profile?(handle: string): Promise + /** Fetch recent posts by a user. */ + userPosts?(handle: string, limit?: number): Promise + /** Attach an annotation to a URL/post. Bluesky-specific. */ + annotate?(targetId: string, text: string, opts?: AnnotateOpts): Promise +} + +/** Per-platform character limits. */ +export const PLATFORM_LIMITS: Record = { + bsky: { chars: 300, threads: true }, + x: { chars: 280, threads: true }, +} diff --git a/social-cli/src/platforms/x.ts b/social-cli/src/platforms/x.ts new file mode 100644 index 0000000..22c0450 --- /dev/null +++ b/social-cli/src/platforms/x.ts @@ -0,0 +1,246 @@ +/** + * X (Twitter) platform implementation. + * Uses twitter-api-v2 npm package. + * Posts via OAuth 1.0a (free tier requires user context). + */ + +import { TwitterApi } from "twitter-api-v2" +import type { + SocialPlatform, + PostOpts, + PostResult, + Notification, + NotifOpts, + SearchResult, + FeedItem, + RateLimitInfo, + ProfileInfo, +} from "./types.js" +import { loadConfig, loadCredentials } from "../config.js" +import { withRetry } from "../util/retry.js" + +let _client: TwitterApi | null = null + +function getClient(): TwitterApi { + if (_client) return _client + + const config = loadConfig() + loadCredentials("x", config) + + const apiKey = process.env.X_API_KEY + const apiSecret = process.env.X_API_SECRET + const accessToken = process.env.X_ACCESS_TOKEN + const accessTokenSecret = process.env.X_ACCESS_TOKEN_SECRET + + if (!apiKey || !apiSecret || !accessToken || !accessTokenSecret) { + throw new Error("X_API_KEY, X_API_SECRET, X_ACCESS_TOKEN, X_ACCESS_TOKEN_SECRET required") + } + + _client = new TwitterApi({ + appKey: apiKey, + appSecret: apiSecret, + accessToken, + accessSecret: accessTokenSecret, + }) + + return _client +} + +export const x: SocialPlatform = { + name: "x", + + async post(text: string, _opts?: PostOpts): Promise { + const client = getClient() + const res = await withRetry(() => client.v2.tweet(text)) + return { + platform: "x", + id: res.data.id, + text: res.data.text, + } + }, + + async reply(targetId: string, text: string, _opts?: PostOpts): Promise { + const client = getClient() + const res = await withRetry(() => client.v2.reply(text, targetId)) + return { + platform: "x", + id: res.data.id, + text: res.data.text, + } + }, + + async thread(posts: string[]): Promise { + const client = getClient() + const results: PostResult[] = [] + let replyTo: string | undefined + + for (const text of posts) { + const res = await withRetry(() => + replyTo ? client.v2.reply(text, replyTo) : client.v2.tweet(text), + ) + + results.push({ + platform: "x", + id: res.data.id, + text: res.data.text, + }) + replyTo = res.data.id + } + + return results + }, + + async notifications(opts?: NotifOpts): Promise<{ notifications: Notification[]; cursor?: string }> { + const client = getClient() + const limit = opts?.limit ?? 20 + + // X uses mentions as the closest equivalent to notifications + const me = await client.v2.me() + const params: Record = { + max_results: Math.min(limit, 100), + "tweet.fields": ["created_at", "author_id", "conversation_id"], + expansions: ["author_id"], + } + // Pass cursor as since_id — X returns only tweets newer than this ID + if (opts?.cursor) params.since_id = opts.cursor + + const mentions = await withRetry(() => client.v2.userMentionTimeline(me.data.id, params)) + + const authors: Record = {} + if (mentions.includes?.users) { + for (const u of mentions.includes.users) { + authors[u.id] = u.username + } + } + + const notifs: Notification[] = [] + for (const tweet of mentions.data?.data ?? []) { + notifs.push({ + id: tweet.id, + platform: "x", + type: "mention", + author: authors[tweet.author_id ?? ""] ?? "unknown", + authorId: tweet.author_id, + postId: tweet.id, + text: tweet.text, + timestamp: tweet.created_at ?? new Date().toISOString(), + }) + } + + // Use the newest tweet's ID as the cursor for the next call + const cursor = notifs.length > 0 ? notifs[0].id : undefined + return { notifications: notifs, cursor } + }, + + async search(query: string, limit = 10): Promise { + const client = getClient() + const apiLimit = Math.max(10, Math.min(limit, 100)) + const res = await withRetry(() => client.v2.search(query, { + max_results: apiLimit, + "tweet.fields": ["created_at", "author_id"], + })) + + return (res.data?.data ?? []).map((t) => ({ + platform: "x", + id: t.id, + author: t.author_id ?? "unknown", + text: t.text, + timestamp: t.created_at ?? "", + })) + }, + + async feed(limit = 50): Promise { + const client = getClient() + const me = await client.v2.me() + const timeline = await withRetry(() => client.v2.homeTimeline({ + max_results: Math.min(limit, 100), + "tweet.fields": ["created_at", "author_id", "public_metrics"], + })) + + return (timeline.data?.data ?? []).map((t) => ({ + platform: "x", + id: t.id, + author: t.author_id ?? "unknown", + text: t.text, + timestamp: t.created_at ?? "", + likeCount: (t.public_metrics as any)?.like_count ?? 0, + replyCount: (t.public_metrics as any)?.reply_count ?? 0, + repostCount: (t.public_metrics as any)?.retweet_count ?? 0, + })) + }, + + async rateLimitStatus(): Promise { + // X rate limits are tracked per-endpoint. + // Return a reasonable default; real tracking comes later. + return { + platform: "x", + remaining: 50, + limit: 50, + resetsAt: new Date(Date.now() + 15 * 60 * 1000).toISOString(), + } + }, + + async delete(targetId: string): Promise { + const client = getClient() + await withRetry(() => client.v2.deleteTweet(targetId)) + }, + + async like(targetId: string): Promise { + const client = getClient() + const me = await client.v2.me() + await withRetry(() => client.v2.like(me.data.id, targetId)) + }, + + async whoami(): Promise { + const client = getClient() + const me = await client.v2.me({ "user.fields": ["public_metrics", "name", "username", "description"] }) + return { + platform: "x", + handle: me.data.username, + displayName: me.data.name, + bio: (me.data as any).description, + followersCount: (me.data.public_metrics as any)?.followers_count, + followingCount: (me.data.public_metrics as any)?.following_count, + postsCount: (me.data.public_metrics as any)?.tweet_count, + } + }, + + async userPosts(handle: string, limit = 20): Promise { + const client = getClient() + const user = await withRetry(() => client.v2.userByUsername(handle)) + if (!user.data) throw new Error(`User not found: ${handle}`) + const timeline = await withRetry(() => client.v2.userTimeline(user.data.id, { + max_results: Math.max(10, Math.min(limit, 100)), + "tweet.fields": ["created_at", "public_metrics"], + })) + return (timeline.data?.data ?? []).map((t) => ({ + platform: "x", + id: t.id, + author: handle, + text: t.text, + timestamp: t.created_at ?? "", + likeCount: (t.public_metrics as any)?.like_count ?? 0, + replyCount: (t.public_metrics as any)?.reply_count ?? 0, + repostCount: (t.public_metrics as any)?.retweet_count ?? 0, + })) + }, + + async profile(handle: string): Promise { + const client = getClient() + const user = await withRetry(() => client.v2.userByUsername(handle, { + "user.fields": ["public_metrics", "name", "username", "description"], + })) + if (!user.data) throw new Error(`User not found: ${handle}`) + return { + platform: "x", + handle: user.data.username, + displayName: user.data.name, + bio: (user.data as any).description, + followersCount: (user.data.public_metrics as any)?.followers_count, + followingCount: (user.data.public_metrics as any)?.following_count, + postsCount: (user.data.public_metrics as any)?.tweet_count, + } + }, + + // X does not support annotations +} diff --git a/social-cli/src/util/fs.ts b/social-cli/src/util/fs.ts new file mode 100644 index 0000000..bb82935 --- /dev/null +++ b/social-cli/src/util/fs.ts @@ -0,0 +1,19 @@ +/** + * Atomic file write: write to .tmp, then rename. + * Prevents half-written files on crash or concurrent access. + * Falls back to direct write for special paths (pipes, devices). + */ + +import { writeFileSync, renameSync, statSync } from "node:fs" + +export function writeFileAtomic(filePath: string, content: string): void { + // Special paths (pipes, /dev/*, -) can't do tmp+rename + if (filePath === "-" || filePath.startsWith("/dev/") || filePath.startsWith("/proc/")) { + writeFileSync(filePath, content) + return + } + + const tmp = `${filePath}.tmp` + writeFileSync(tmp, content) + renameSync(tmp, filePath) +} diff --git a/social-cli/src/util/retry.ts b/social-cli/src/util/retry.ts new file mode 100644 index 0000000..171ecd9 --- /dev/null +++ b/social-cli/src/util/retry.ts @@ -0,0 +1,67 @@ +/** + * Retry with exponential backoff. + * Retries on network errors, 429, 5xx. Does not retry 4xx auth/validation. + */ + +export interface RetryOpts { + maxAttempts?: number + baseDelay?: number // ms + maxDelay?: number // ms +} + +const RETRYABLE_CODES = new Set(["ECONNRESET", "ETIMEDOUT", "ENOTFOUND", "EPIPE", "EAI_AGAIN"]) + +function isRetryable(err: unknown): boolean { + if (err instanceof Error) { + // Network-level errors + if ("code" in err && RETRYABLE_CODES.has((err as any).code)) return true + + // HTTP status-based errors + const status = (err as any).status ?? (err as any).statusCode ?? (err as any).data?.status + if (typeof status === "number") { + if (status === 429) return true // rate limited + if (status >= 500) return true // server error + } + + // twitter-api-v2 wraps rate limits + if (err.message?.includes("429") || err.message?.includes("Rate limit")) return true + } + return false +} + +function getRetryAfter(err: unknown): number | null { + const headers = (err as any)?.headers ?? (err as any)?.rateLimit + if (headers?.["retry-after"]) { + const secs = parseInt(headers["retry-after"], 10) + if (!isNaN(secs)) return secs * 1000 + } + if (headers?.reset) { + const resetAt = typeof headers.reset === "number" ? headers.reset * 1000 : Date.parse(headers.reset) + if (!isNaN(resetAt)) return Math.max(0, resetAt - Date.now()) + } + return null +} + +export async function withRetry( + fn: () => Promise, + opts?: RetryOpts, +): Promise { + const maxAttempts = opts?.maxAttempts ?? 3 + const baseDelay = opts?.baseDelay ?? 1000 + const maxDelay = opts?.maxDelay ?? 30000 + + let lastErr: unknown + for (let attempt = 0; attempt < maxAttempts; attempt++) { + try { + return await fn() + } catch (err) { + lastErr = err + if (attempt >= maxAttempts - 1 || !isRetryable(err)) throw err + + const retryAfter = getRetryAfter(err) + const delay = retryAfter ?? Math.min(baseDelay * 2 ** attempt, maxDelay) + await new Promise((r) => setTimeout(r, delay)) + } + } + throw lastErr +} diff --git a/social-cli/src/util/validate.ts b/social-cli/src/util/validate.ts new file mode 100644 index 0000000..6c78390 --- /dev/null +++ b/social-cli/src/util/validate.ts @@ -0,0 +1,27 @@ +/** + * Text validation for quick commands. + * Checks platform char limits before posting. + */ + +import { PLATFORM_LIMITS } from "../platforms/types.js" + +export function validateText(platform: string, text: string): void { + const limit = PLATFORM_LIMITS[platform] + if (!limit) return // unknown platform, let the API decide + + if (text.length > limit.chars) { + console.error(`Text exceeds ${platform} limit: ${text.length}/${limit.chars} chars`) + process.exit(1) + } +} + +export function validateTexts(platform: string, texts: string[]): void { + for (let i = 0; i < texts.length; i++) { + const limit = PLATFORM_LIMITS[platform] + if (!limit) return + if (texts[i].length > limit.chars) { + console.error(`Post ${i + 1} exceeds ${platform} limit: ${texts[i].length}/${limit.chars} chars`) + process.exit(1) + } + } +} diff --git a/social-cli/src/util/yaml.test.ts b/social-cli/src/util/yaml.test.ts new file mode 100644 index 0000000..dab6730 --- /dev/null +++ b/social-cli/src/util/yaml.test.ts @@ -0,0 +1,79 @@ +/** + * YAML round-trip test. + * Ensures hostile content survives serialize → parse without corruption. + */ + +import { describe, it, expect } from "vitest" +import { stringify, parse } from "yaml" + +const HOSTILE_TEXTS = [ + "---", + "key: value", + "# this looks like a comment", + "{json: true}", + "[array, items]", + "null", + "true", + "false", + "123", + "1.5e10", + "!!str exploit", + "text with\nnewlines\nand\ttabs", + "colon: in middle", + "trailing colon:", + ": leading colon", + "emoji 🎉 and unicode: café naïve résumé", + 'single "quotes" and \'apostrophes\'', + "back\\slash", + "pipe | character", + "> folded indicator", + "| literal indicator", + "& anchor", + "* alias", + "% directive", + "@ at sign", + "` backtick", + "", + " leading space", + "trailing space ", + "multi\n---\ndocument\n...\nseparators", +] + +describe("YAML round-trip", () => { + it("preserves hostile text in a notifications array", () => { + const notifications = HOSTILE_TEXTS.map((text, i) => ({ + id: `test-${i}`, + platform: "bsky", + type: "mention", + author: "test.bsky.social", + postId: `post-${i}`, + text, + timestamp: "2026-01-01T00:00:00Z", + })) + + const serialized = stringify({ notifications }, { lineWidth: 120 }) + const parsed = parse(serialized) as { notifications: typeof notifications } + + expect(parsed.notifications).toHaveLength(notifications.length) + + for (let i = 0; i < notifications.length; i++) { + expect(parsed.notifications[i].text).toBe(notifications[i].text) + expect(parsed.notifications[i].id).toBe(notifications[i].id) + } + }) + + it("preserves hostile text in dispatch actions", () => { + const dispatch = HOSTILE_TEXTS.map((text) => ({ + post: { text, platforms: ["bsky"] }, + })) + + const serialized = stringify({ dispatch }, { lineWidth: 120 }) + const parsed = parse(serialized) as { dispatch: typeof dispatch } + + expect(parsed.dispatch).toHaveLength(dispatch.length) + + for (let i = 0; i < dispatch.length; i++) { + expect(parsed.dispatch[i].post.text).toBe(dispatch[i].post.text) + } + }) +}) diff --git a/social-cli/tsconfig.json b/social-cli/tsconfig.json new file mode 100644 index 0000000..fa43fd4 --- /dev/null +++ b/social-cli/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +}