From 48a03adae013e03db6f374c81197d50653d26798 Mon Sep 17 00:00:00 2001 From: Giovanni Laquidara Date: Thu, 11 Jun 2026 16:02:32 +0100 Subject: [PATCH 01/13] feat: add TUI dashboard, renderer, and agent infrastructure --- .gitignore | 1 + AGENT.md | 112 ++++++++ biome.json | 13 + bun.lock | 20 +- docs/OPENCLAW_SETUP.md | 290 +++++++++++++++++++++ docs/TUI_PATTERNS_RESEARCH.md | 165 ++++++++++++ package.json | 5 +- sources/errors.ts | 35 +++ sources/main.ts | 7 + sources/tui/dashboard.ts | 416 ++++++++++++++++++++++++++++++ sources/tui/index.ts | 16 ++ sources/tui/renderer.ts | 468 ++++++++++++++++++++++++++++++++++ sources/utils/format.ts | 52 ++++ 13 files changed, 1598 insertions(+), 2 deletions(-) create mode 100644 AGENT.md create mode 100644 biome.json create mode 100644 docs/OPENCLAW_SETUP.md create mode 100644 docs/TUI_PATTERNS_RESEARCH.md create mode 100644 sources/errors.ts create mode 100644 sources/tui/dashboard.ts create mode 100644 sources/tui/index.ts create mode 100644 sources/tui/renderer.ts create mode 100644 sources/utils/format.ts diff --git a/.gitignore b/.gitignore index 3eb76d3..575506c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ /node_modules /bun.lockb .DS_Store +.context/ diff --git a/AGENT.md b/AGENT.md new file mode 100644 index 0000000..b5fdd6b --- /dev/null +++ b/AGENT.md @@ -0,0 +1,112 @@ +# Bee CLI Agent Instructions + +Instructions for AI agents (OpenClaw, Hermes, etc.) to use bee-cli autonomously. + +## Setup + +```bash +export BEE_OUTPUT_FORMAT=json +export BEE_FORCE_FILE_STORE=1 +``` + +Verify authentication: +```bash +bee status --json +``` + +If `"authenticated": false`, run `bee login` (requires human interaction once). + +Discover available commands: +```bash +bee --describe +``` + +## Usage Pattern + +1. Set `BEE_OUTPUT_FORMAT=json` in your environment (all output becomes JSON). +2. Call `bee validate [subcommand] [--flags]` before mutating commands to pre-check. +3. Read stdout as JSON. All commands return structured data. +4. Check exit code after every call: + - `0` = success, process the JSON response + - `2` = auth error, run `bee login` or refresh token + - `3` = invalid arguments, fix the flags and retry + - `4` = API/network error, retry with exponential backoff + - `5` = rate limited, wait and retry + - `1` = general error, report to user +5. On non-zero exit, read stderr for structured error JSON: + ```json + {"error": "message", "code": 3, "recoverable": false, "suggestion": "..."} + ``` + +## Commands + +### Read operations (no side effects) + +| Command | Description | +|---------|-------------| +| `bee facts list [--limit N] [--cursor C]` | List personal facts | +| `bee facts get ` | Get a single fact | +| `bee todos list [--limit N] [--cursor C]` | List todos | +| `bee todos get ` | Get a single todo | +| `bee conversations list [--limit N] [--cursor C] [--bookmarked]` | List conversations | +| `bee conversations get ` | Get conversation detail | +| `bee daily list [--limit N] [--cursor C]` | List daily summaries | +| `bee daily get ` | Get daily summary detail | +| `bee journals list [--limit N] [--cursor C]` | List journal entries | +| `bee search --query [--type conversations\|emails\|calendar] [--neural]` | Search across data | +| `bee today` | Today's calendar + emails brief | +| `bee now` | Recent conversations (last 10 hours) | +| `bee changed [--cursor C]` | Changes since last check | +| `bee insights list [--category C]` | List insights | +| `bee me` | User profile | +| `bee status` | Auth and connection status | +| `bee activity [--limit N]` | Recent activity | +| `bee locations [--limit N]` | Location history | +| `bee photos [--limit N]` | Photos with AI descriptions | + +### Write operations (have side effects) + +| Command | Description | +|---------|-------------| +| `bee facts create --text ` | Create a fact | +| `bee facts update --text [--confirmed true\|false]` | Update a fact | +| `bee facts delete ` | Delete a fact | +| `bee todos create --text [--priority N] [--alarm-at ]` | Create a todo | +| `bee todos update [--text T] [--completed true\|false]` | Update a todo | +| `bee todos delete ` | Delete a todo | + +### Utility + +| Command | Description | +|---------|-------------| +| `bee --describe` | Full command schema (JSON blob for discovery) | +| `bee validate [args]` | Pre-validate without executing | +| `bee status --json` | Structured auth/connection status | +| `bee sync --output [--only facts\|todos\|daily\|conversations]` | Export to markdown files | +| `bee stream [--types T] [--json\|--agent]` | Real-time event stream (SSE) | + +## Pagination + +List commands return `next_cursor` in the response. Pass it back: +```bash +bee facts list --cursor "v1-1779315328643-10583865" +``` + +## Output Modes + +| Flag | Effect | +|------|--------| +| (none with `BEE_OUTPUT_FORMAT=json`) | JSON output (agent default) | +| `--pretty` | Human-readable markdown | +| `--minimal` | Compact JSON (strips timezone, no indentation) | +| `--json` | Explicit JSON (same as env var) | + +## Error Recovery + +``` +Exit 2 (auth) → bee login (needs human), then retry +Exit 3 (args) → fix command flags, retry immediately +Exit 4 (network) → exponential backoff: 1s, 2s, 4s, 8s, max 3 retries +Exit 5 (rate) → wait 60s, then retry +Exit 1 (general) → log and report to user +``` diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..8050235 --- /dev/null +++ b/biome.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "organizeImports": { "enabled": true }, + "linter": { + "enabled": true, + "rules": { "recommended": true } + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2 + } +} diff --git a/bun.lock b/bun.lock index fbb8d03..4086232 100644 --- a/bun.lock +++ b/bun.lock @@ -1,6 +1,5 @@ { "lockfileVersion": 1, - "configVersion": 1, "workspaces": { "": { "name": "bee-cli", @@ -11,6 +10,7 @@ "tweetnacl": "^1.0.3", }, "devDependencies": { + "@biomejs/biome": "1.9.4", "@types/qrcode": "^1.5.5", "bun-types": "^1.3.8", "typescript": "^5.7.3", @@ -18,6 +18,24 @@ }, }, "packages": { + "@biomejs/biome": ["@biomejs/biome@1.9.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "1.9.4", "@biomejs/cli-darwin-x64": "1.9.4", "@biomejs/cli-linux-arm64": "1.9.4", "@biomejs/cli-linux-arm64-musl": "1.9.4", "@biomejs/cli-linux-x64": "1.9.4", "@biomejs/cli-linux-x64-musl": "1.9.4", "@biomejs/cli-win32-arm64": "1.9.4", "@biomejs/cli-win32-x64": "1.9.4" }, "bin": { "biome": "bin/biome" } }, "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog=="], + + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@1.9.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw=="], + + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@1.9.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg=="], + + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g=="], + + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA=="], + + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg=="], + + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg=="], + + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@1.9.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg=="], + + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@1.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA=="], + "@types/node": ["@types/node@22.19.7", "https://amazon-149122183214.d.codeartifact.us-west-2.amazonaws.com/npm/shared/@types/node/-/node-22.19.7.tgz", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw=="], "@types/qrcode": ["@types/qrcode@1.5.6", "https://amazon-149122183214.d.codeartifact.us-west-2.amazonaws.com/npm/shared/@types/qrcode/-/qrcode-1.5.6.tgz", { "dependencies": { "@types/node": "*" } }, "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw=="], diff --git a/docs/OPENCLAW_SETUP.md b/docs/OPENCLAW_SETUP.md new file mode 100644 index 0000000..4498a05 --- /dev/null +++ b/docs/OPENCLAW_SETUP.md @@ -0,0 +1,290 @@ +# Running Bee CLI with OpenClaw + +This guide explains how to set up bee-cli for use as a tool by an OpenClaw agent, enabling autonomous access to your Bee personal AI data (conversations, facts, todos, health, calendar, emails, and more). + +--- + +## Prerequisites + +- **Node.js 18+** or **Bun 1.0+** installed +- A **Bee account** (sign up at bee.computer) +- **OpenClaw** configured and running + +--- + +## 1. Install Bee CLI + +### Option A: From npm (recommended) + +```bash +npm install -g @beeai/cli +``` + +### Option B: From source + +```bash +git clone https://github.com/giolaq/bee-cli.git +cd bee-cli +bun install +bun run build +# Binary at ./dist/bee +``` + +### Option C: Direct with Bun (development) + +```bash +git clone https://github.com/giolaq/bee-cli.git +cd bee-cli +bun install +# Run directly: +bun ./sources/main.ts +``` + +--- + +## 2. Authenticate + +Bee CLI requires a one-time human login to obtain an API token: + +```bash +bee login +``` + +This opens a browser for pairing. Follow the prompts. Once complete, the token is stored locally at `~/.bee/token-prod`. + +Verify authentication: + +```bash +bee status +``` + +Expected output: +``` +API: production (https://app-api-developer.ce.bee.amazon.dev/) +Token: eyJ4...Xm2k +Verified as Giovanni Laquidara (id 33070). +``` + +### Headless environments + +If running in a CI/headless environment, copy the token file from an authenticated machine: + +```bash +# On authenticated machine: +cat ~/.bee/token-prod + +# On headless machine: +mkdir -p ~/.bee && chmod 700 ~/.bee +echo "YOUR_TOKEN_HERE" > ~/.bee/token-prod +chmod 600 ~/.bee/token-prod +export BEE_FORCE_FILE_STORE=1 +``` + +--- + +## 3. Configure for Agent Mode + +Set these environment variables in your OpenClaw agent configuration: + +```bash +export BEE_OUTPUT_FORMAT=json # All output as structured JSON +export BEE_FORCE_FILE_STORE=1 # Use file-based token (no keychain dependency) +``` + +These ensure: +- Every command returns parseable JSON on stdout +- Errors return structured JSON on stderr +- No interactive prompts or UI formatting + +--- + +## 4. OpenClaw Configuration + +### Tool Definition + +Add bee-cli as a tool in your OpenClaw agent's configuration. The tool wraps shell execution of `bee` commands. + +```yaml +# openclaw-agent.yaml +tools: + - name: bee + description: | + Access the user's Bee personal AI data. Manages conversations, facts, + todos, journals, health data, calendar, emails, and more. + + SETUP: Run `bee --describe` first to discover all available commands. + Run `bee validate [args]` before mutating operations. + + EXIT CODES: + 0 = success (read stdout as JSON) + 2 = auth error (suggest re-login) + 3 = invalid arguments (fix and retry) + 4 = network/API error (retry with backoff) + 5 = rate limited (wait 60s) + type: shell + command: bee + env: + BEE_OUTPUT_FORMAT: json + BEE_FORCE_FILE_STORE: "1" +``` + +### Agent System Prompt + +Include this in your agent's system prompt so it knows how to use bee-cli: + +``` +You have access to the `bee` CLI tool which manages the user's personal AI data. + +## Discovery +Run `bee --describe` to get a JSON schema of all available commands, their parameters, +and whether they have side effects. + +## Usage Protocol +1. Before any write operation, run: bee validate [args] +2. Execute: bee [args] +3. Read stdout (JSON). Check exit code. +4. On error, read stderr for: {"error": "...", "code": N, "recoverable": bool, "suggestion": "..."} + +## Exit Code Handling +- 0: Success. Parse stdout JSON. +- 2: Auth expired. Tell user to run `bee login`. +- 3: Bad arguments. Fix flags and retry immediately. +- 4: Network/API error. Retry up to 3 times with backoff (1s, 2s, 4s). +- 5: Rate limited. Wait 60 seconds, then retry. +- 1: General error. Report to user. + +## Key Commands +- bee facts list/create/update/delete — personal knowledge base +- bee todos list/create/update/delete — task management +- bee conversations list/get — recorded conversations +- bee search --query "..." — semantic search across all data +- bee changed — what's new since last check +- bee today — today's calendar + email brief +- bee now — recent conversations (last 10 hours) +- bee daily list/get — daily narrative summaries +- bee journals list — voice journal entries +- bee health heart-rate|steps|sleep|alerts — health data +- bee calendar list — calendar events +- bee emails list — email inbox +- bee sync --output ./data — export everything to markdown files + +## Pagination +List commands may return "next_cursor" in the response. +Pass it back: bee list --cursor "cursor_value" + +## Important +- Always use `bee validate` before create/update/delete operations +- The `bee stream` command is for real-time SSE events (long-running) +- The `bee sync` command writes files to disk (specify --output directory) +``` + +--- + +## 5. Verify Agent Setup + +Run these commands to confirm everything works: + +```bash +# 1. Check auth +bee status --json +# Expected: {"authenticated":true,"user_id":...,"api_reachable":true} + +# 2. Discover commands +bee --describe | head -20 +# Expected: JSON blob with version, auth_status, commands object + +# 3. Validate a command +bee validate facts list --limit 5 +# Expected: {"valid":true} + +# 4. Execute a command +bee facts list --limit 2 +# Expected: {"facts":[...],"next_cursor":...,"timezone":"..."} + +# 5. Test error handling +bee facts create +# Expected exit 3, stderr: {"error":"Missing fact text...","code":3,...} +``` + +--- + +## 6. Example Agent Session + +Here's what a typical OpenClaw agent session looks like using bee-cli: + +``` +Agent: I'll check what the user has been up to today. + +[Executes: bee today] +→ {"calendar_events":[],"emails":[],"timezone":"America/Los_Angeles"} + +Agent: No calendar or emails today. Let me check recent conversations. + +[Executes: bee now] +→ {"since":1779275504,"until":1779311504,"timezone":"America/Los_Angeles","conversations":[]} + +Agent: No recent conversations. Let me check what changed since last time. + +[Executes: bee changed] +→ {"meta":{"next_cursor":"v1-...","updated":true},"facts":[{"id":10580853,"text":"I dance bachata",...}],"todos":[],...} + +Agent: I see a new fact was recorded. The user likes to dance bachata. + Let me search for related conversations. + +[Executes: bee validate search --query "bachata"] +→ {"valid":true} + +[Executes: bee search --query "bachata"] +→ {"results":[...],"timezone":"America/Los_Angeles"} + +Agent: Found a conversation about dancing. Let me create a todo reminder. + +[Executes: bee validate todos create --text "Look up bachata classes nearby"] +→ {"valid":true} + +[Executes: bee todos create --text "Look up bachata classes nearby"] +→ {"id":27571800,"text":"Look up bachata classes nearby","completed":false,...} + +Agent: Done! Created a todo to look up bachata classes. +``` + +--- + +## 7. Troubleshooting + +### "Not authenticated" (exit code 2) + +The token has expired or is missing. A human needs to run `bee login` once to re-authenticate. + +### "Request failed with status 500" (exit code 4) + +The Bee API is having issues. Retry with backoff. If persistent, the feature may not be enabled for this account (e.g., calendar/email require linking a provider in the Bee app). + +### "--limit must be a positive integer" (exit code 3) + +The agent sent invalid arguments. Check the `--describe` output for correct parameter types. + +### Commands returning 404 + +Some endpoints (contacts, photos, screen, checkins, health, products) require the Bee Pioneer wearable to be connected and actively recording. If the user doesn't have these features enabled, the API returns 404. + +### Token storage + +Tokens are stored at: +- `~/.bee/token-prod` (production) +- `~/.bee/token-staging` (staging) + +Ensure the directory has correct permissions: `chmod 700 ~/.bee` + +--- + +## 8. Reference + +| Resource | Location | +|----------|----------| +| Full agent instructions | `AGENT.md` in repo root | +| Command schema | `bee --describe` (runtime) | +| Validation | `bee validate [args]` | +| Source code | `sources/commands/*/index.ts` | +| Mock server (for testing) | `bun ./mock-server.ts` | +| API endpoint comparison | See `API_ENDPOINTS_COMPARISON.md` (if generated) | diff --git a/docs/TUI_PATTERNS_RESEARCH.md b/docs/TUI_PATTERNS_RESEARCH.md new file mode 100644 index 0000000..4aca300 --- /dev/null +++ b/docs/TUI_PATTERNS_RESEARCH.md @@ -0,0 +1,165 @@ +# Plan: TUI Research Findings — Design Patterns for Bee CLI + +## Context + +Feedback received: research what makes TUIs great in (1) frontier coding agents (Gemini CLI, Claude Code, OpenCode, Kiro) and (2) SaaS CLIs closer to bee-cli's scope (Stripe, 1Password, Tailscale, gh, Charm tools). The coding agents are heavier workbenches; the SaaS CLIs are closer to what bee-cli should be. + +**Goal:** Research only. Compile findings into a reference document for future TUI improvements. No code changes. + +--- + +## Research Results + +### Category 1: Frontier Coding Agent TUIs + +Researched: Claude Code, OpenCode (OpenTUI), Gemini CLI, Kiro + +#### Key Patterns + +**1. Adaptive Output Detection** +All tools detect `process.stdout.isTTY` to decide between rich TUI and plain output. Support multiple formats (JSON, text, minimal) for piping. This is exactly what bee-cli already does with `shouldUseTui()`. + +**2. Box-Based Panels (not full-screen)** +Rather than full-screen frameworks, these tools use Unicode box rendering (╭─╮ │ ╰─╯) for self-contained panels. Panels are independent units sized to terminal width. Avoids heavyweight TUI libraries. + +**3. Semantic ANSI Color Theme** +Consistent palette mapped to developer intent: +- Cyan (36): content, data values +- Green (32): success, active, confirmed +- Yellow (33): pending, in-progress, warnings +- Red (31): errors, inactive +- Gray (90): metadata, secondary info +- Bold white (1;37): section headings + +Uses raw ANSI codes, not external libraries. + +**4. In-Place Progress (no scrolling)** +Braille spinners (⠋⠙⠹⠸) at 80ms. Multi-task progress uses cursor movement (`\x1b[nA`) to overwrite previous lines rather than scrolling the terminal. + +**5. Smart Truncation** +Strip ANSI before measuring width. Truncate mid-line preserving color codes. Virtual scrolling (track offset, render visible window only). + +**6. Two-Pane Dashboard** +Fixed narrow left pane (navigation), scrollable right pane (content). Expand items for detail. Vim keys + arrow keys. + +**7. Error Boxes with Metadata** +Error display includes: icon (✗), message, error code, recoverability flag, and actionable suggestion in a dedicated box. + +**8. What Makes Them "Good"** +- Zero dependencies (raw ANSI) +- Graceful degradation (works in pipes, CI) +- Responsive to terminal width +- Information density over flashiness +- Fast redraws via cursor positioning +- Dashboard is optional; commands work standalone + +--- + +### Category 2: SaaS CLI TUI Patterns + +Researched: Stripe CLI, GitHub CLI (gh), 1Password (op), Tailscale, Charm tools (lipgloss, bubbletea, gum) + +#### Key Patterns + +**1. One-Shot Default, Interactive Optional** +Default behavior: print and exit. Users expect to pipe output. Interactive mode only when explicitly requested (`--interactive`) or when piping is unavailable. Never force modal UI for data commands. + +**2. Conservative Color Usage** +- Green: success/positive only +- Red: errors only +- Cyan/blue: interactive prompts +- Dim/faint: timestamps, IDs, supplementary info +- Respect `NO_COLOR` env var +- Auto-downsample to 256/16 color terminals + +**3. Data Display Strategy** +- Lists: compact tables with consistent column alignment +- Large datasets: limit by default (e.g., 10 items), show `↓ 47 more` with `--all` flag +- Key-value: left-align keys, dim metadata +- Never paginate interactively for data commands + +**4. Flags for Output Control** +- `--json` for machine-readable +- `--limit N` for row count +- `--wide` for expanded columns +- `--no-limit` / `--all` for full dataset +- `--depth N` for nested data + +**5. Error Display** +- Print to stderr (not stdout) +- One-line format: `Error: [brief message]` +- Add `--help` suggestion on ambiguous commands +- Structured exit codes for automation + +**6. Progress/Status** +- Quick ops (<500ms): no indicator needed +- Long ops: simple spinner +- Streaming: timestamp-prefixed log lines (Stripe webhook listener model) + +**7. Lightweight Feel** +- Minimize startup time (no heavy frameworks for simple commands) +- Dense output by default, `--verbose` for extra +- Borders/boxes used sparingly (one per logical section, not every element) +- Fast commands should feel instant + +--- + +## Comparison: Bee CLI Current State vs Best Practices + +| Pattern | Bee CLI Current | Best Practice | Gap | +|---------|----------------|---------------|-----| +| TTY detection | ✅ `shouldUseTui()` | Adaptive | None | +| Color scheme | ✅ Semantic ANSI | Consistent palette | None | +| Box rendering | ✅ Unicode borders | Self-contained panels | None | +| One-shot default | ⚠️ Dashboard is separate cmd | One-shot for data cmds | Minor | +| `NO_COLOR` support | ❌ Missing | Respect env var | Gap | +| Data overflow | ✅ Truncation + scroll | `--all` / count indicator | Could add | +| Error on stderr | ✅ In JSON mode | Always stderr for errors | None | +| Progress spinners | ❌ Not in commands | Spinner for API calls | Gap | +| `--wide` flag | ❌ Missing | Expanded output option | Gap | +| Startup speed | ✅ Fast (Bun) | <100ms for simple cmds | None | +| Vim + arrow keys | ✅ In dashboard | Dual bindings | None | +| Borders sparingly | ⚠️ Box around everything | One per section max | Minor | + +--- + +## Top Recommendations (for future implementation) + +### High Priority (matches SaaS CLI patterns) +1. **Add `NO_COLOR` env var support** — disable all ANSI when set (standard: https://no-color.org/) +2. **Add item count indicator** — when output is limited, show "showing 10 of 47, use --all for full list" +3. **Add loading spinners** — for API calls taking >500ms, show inline spinner +4. **Lighten the box usage** — in one-shot TUI mode, use boxes only for the outer container, not every sub-section + +### Medium Priority (good DX) +5. **Add `--wide` flag** — show all fields without truncation (alternative to `--json`) +6. **Add streaming progress for sync** — show real-time progress during `bee sync` with spinner per target +7. **Respect terminal resize** — listen to SIGWINCH in dashboard mode + +### Low Priority (frontier agent patterns, heavier than needed) +8. **In-place multi-task progress** — cursor movement to update multiple progress lines (for sync) +9. **Syntax highlighting for conversation transcripts** — tree-sitter for code blocks in conversations + +--- + +## Key Insight from the Feedback + +> "Those are full workbenches built to invoke other tools, so they're heavier than what you're building. Worth comparing against SaaS CLIs too. Lean a bit slimmer than the frontier coding agents." + +**Translation for bee-cli:** +- Don't build toward OpenCode/Claude Code's full-screen TUI complexity +- The current two-pane dashboard is fine as an *optional* mode +- Default commands should feel like `gh` or `stripe`: fast, one-shot, pipe-friendly +- The box renderer (`sources/tui/renderer.ts`) is the right weight for one-shot output +- The dashboard (`sources/tui/dashboard.ts`) is the right weight for interactive exploration +- Don't add bubbletea/lipgloss dependencies — raw ANSI is the correct choice for this project size + +--- + +## Deliverable + +Write these findings to `docs/TUI_PATTERNS_RESEARCH.md` in the project. + +## Verification + +N/A — this is a research document, no code changes needed. diff --git a/package.json b/package.json index 46466eb..ce1bd89 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,9 @@ "prepack": "bun run build:lib && bun run build:all", "postinstall": "node ./scripts/postinstall.js", "release": "bash ./scripts/release.sh", - "typecheck": "tsc --noEmit" + "typecheck": "tsc --noEmit", + "lint": "biome check sources/", + "format": "biome format --write sources/" }, "dependencies": { "date-fns": "^4.1.0", @@ -42,6 +44,7 @@ "tweetnacl": "^1.0.3" }, "devDependencies": { + "@biomejs/biome": "1.9.4", "@types/qrcode": "^1.5.5", "bun-types": "^1.3.8", "typescript": "^5.7.3" diff --git a/sources/errors.ts b/sources/errors.ts new file mode 100644 index 0000000..6840f73 --- /dev/null +++ b/sources/errors.ts @@ -0,0 +1,35 @@ +export class BeeError extends Error { + constructor( + message: string, + public readonly exitCode: number, + public readonly recoverable: boolean, + public readonly suggestion?: string + ) { + super(message); + this.name = this.constructor.name; + } +} + +export class AuthError extends BeeError { + constructor(message: string, suggestion?: string) { + super(message, 2, true, suggestion ?? "Run bee login"); + } +} + +export class ValidationError extends BeeError { + constructor(message: string) { + super(message, 3, false); + } +} + +export class ApiError extends BeeError { + constructor(message: string, suggestion?: string) { + super(message, 4, true, suggestion ?? "Retry with backoff"); + } +} + +export class RateLimitError extends BeeError { + constructor(message: string) { + super(message, 5, true, "Wait and retry"); + } +} diff --git a/sources/main.ts b/sources/main.ts index abba982..ef4ff4f 100644 --- a/sources/main.ts +++ b/sources/main.ts @@ -24,6 +24,7 @@ import { todayCommand } from "@/commands/today"; import { versionCommand } from "@/commands/version"; import type { Environment } from "@/environment"; import { createCommandContext } from "@/context"; +import { startDashboard } from "@/tui/dashboard"; const BIN = "bee"; @@ -79,6 +80,7 @@ function printHelp(): void { console.log(` ${command.name} ${command.description}${aliasText}`); } + console.log(` dashboard Interactive TUI dashboard (alias: ui)`); console.log(""); console.log(`Run \"${BIN} --help\" for command-specific help.`); } @@ -105,6 +107,11 @@ async function runCli(): Promise { return; } + if (firstArg === "dashboard" || firstArg === "ui") { + await startDashboard(); + return; + } + const commandName = firstArg; const command = commandIndex.get(commandName); diff --git a/sources/tui/dashboard.ts b/sources/tui/dashboard.ts new file mode 100644 index 0000000..e76708d --- /dev/null +++ b/sources/tui/dashboard.ts @@ -0,0 +1,416 @@ +import { requestClientJson } from "@/client/clientApi"; +import { createCommandContext } from "@/context"; +import type { CommandContext } from "@/commands/types"; + +type MenuItem = { + label: string; + key: string; + fetch: (ctx: CommandContext) => Promise; +}; + +type ContentItem = { + title: string; + detail?: string; + color?: string; +}; + +const MENU_COL_WIDTH = 22; + +function stripAnsi(s: string): string { + return s.replace(/\x1b\[[0-9;]*m/g, ""); +} + +function padVisible(s: string, width: number): string { + const visible = stripAnsi(s).length; + const pad = Math.max(0, width - visible); + return s + " ".repeat(pad); +} + +function truncateVisible(s: string, maxWidth: number): string { + if (maxWidth <= 0) return ""; + const stripped = stripAnsi(s); + if (stripped.length <= maxWidth) return s; + + let visibleCount = 0; + let i = 0; + while (i < s.length && visibleCount < maxWidth - 1) { + if (s[i] === "\x1b") { + const end = s.indexOf("m", i); + if (end !== -1) { i = end + 1; continue; } + } + visibleCount++; + i++; + } + return s.slice(0, i) + "\x1b[0m"; +} + +const MENU_ITEMS: MenuItem[] = [ + { label: "Profile", key: "me", fetch: fetchProfile }, + { label: "Status", key: "status", fetch: fetchStatus }, + { label: "Facts", key: "facts", fetch: fetchFacts }, + { label: "Todos", key: "todos", fetch: fetchTodos }, + { label: "Conversations", key: "conversations", fetch: fetchConversations }, + { label: "Daily Summaries", key: "daily", fetch: fetchDaily }, + { label: "Journals", key: "journals", fetch: fetchJournals }, + { label: "Insights", key: "insights", fetch: fetchInsights }, + { label: "Today", key: "today", fetch: fetchToday }, + { label: "Now", key: "now", fetch: fetchNow }, + { label: "Changed", key: "changed", fetch: fetchChanged }, + { label: "Activity", key: "activity", fetch: fetchActivity }, + { label: "Locations", key: "locations", fetch: fetchLocations }, + { label: "Photos", key: "photos", fetch: fetchPhotos }, +]; + +async function fetchProfile(ctx: CommandContext): Promise { + const data = await requestClientJson(ctx, "/v1/me", { method: "GET" }) as Record; + return [ + { title: `${data["first_name"]} ${data["last_name"]}`, detail: `ID: ${data["id"]}\nTimezone: ${data["timezone"]}`, color: "36" }, + ]; +} + +async function fetchStatus(ctx: CommandContext): Promise { + const data = await requestClientJson(ctx, "/v1/status", { method: "GET" }) as Record; + return [ + { title: `Authenticated: ${data["authenticated"] ?? "unknown"}`, color: "32" }, + { title: `Environment: ${ctx.env}`, color: "36" }, + { title: `Proxy: ${ctx.client.isProxy ? "yes" : "no"}`, color: "90" }, + ]; +} + +async function fetchFacts(ctx: CommandContext): Promise { + const data = await requestClientJson(ctx, "/v1/facts?limit=20", { method: "GET" }) as { facts: Array<{ id: number; text: string; tags: string[]; confirmed: boolean }> }; + return data.facts.map(f => ({ + title: `${f.confirmed ? "●" : "○"} ${f.text}`, + detail: `ID: ${f.id}\nTags: ${f.tags.join(", ") || "(none)"}\nConfirmed: ${f.confirmed}`, + color: f.confirmed ? "36" : "90", + })); +} + +async function fetchTodos(ctx: CommandContext): Promise { + const data = await requestClientJson(ctx, "/v1/todos?limit=20", { method: "GET" }) as { todos: Array<{ id: number; text: string; completed: boolean; alarm_at: number | null; created_at: number }> }; + return data.todos.map(t => ({ + title: `${t.completed ? "●" : "○"} ${t.text}`, + detail: `ID: ${t.id}\nCompleted: ${t.completed}\nAlarm: ${t.alarm_at ? new Date(t.alarm_at).toLocaleString() : "(none)"}\nCreated: ${new Date(t.created_at).toLocaleString()}`, + color: t.completed ? "90" : "36", + })); +} + +async function fetchConversations(ctx: CommandContext): Promise { + const data = await requestClientJson(ctx, "/v1/conversations?limit=10", { method: "GET" }) as { conversations: Array<{ id: number; short_summary: string | null; state: string; start_time: number; end_time: number | null }> }; + return data.conversations.map(conv => { + const date = new Date(conv.start_time).toLocaleDateString("en-CA"); + const summary = conv.short_summary ?? "(processing...)"; + return { + title: `[${date}] ${summary}`, + detail: `ID: ${conv.id}\nState: ${conv.state}\nStart: ${new Date(conv.start_time).toLocaleString()}\nEnd: ${conv.end_time ? new Date(conv.end_time).toLocaleString() : "(ongoing)"}`, + color: conv.state === "COMPLETED" ? "32" : conv.state === "CAPTURING" ? "33" : "90", + }; + }); +} + +async function fetchDaily(ctx: CommandContext): Promise { + const data = await requestClientJson(ctx, "/v1/daily?limit=5", { method: "GET" }) as { daily_summaries: Array<{ id: number; short_summary: string; date_time: number }> }; + return data.daily_summaries.map(d => ({ + title: `[${new Date(d.date_time).toLocaleDateString("en-CA")}] ${d.short_summary.slice(0, 50)}`, + detail: `ID: ${d.id}\nDate: ${new Date(d.date_time).toLocaleDateString("en-CA")}\n\n${d.short_summary}`, + color: "36", + })); +} + +async function fetchJournals(ctx: CommandContext): Promise { + const data = await requestClientJson(ctx, "/v1/journals?limit=10", { method: "GET" }) as { journals: Array<{ id: string; text: string; state: string; created_at: number }> }; + return data.journals.map(j => ({ + title: `[${j.state}] ${j.text.slice(0, 40)}`, + detail: `ID: ${j.id}\nState: ${j.state}\nCreated: ${new Date(j.created_at).toLocaleString()}\n\n${j.text}`, + color: "36", + })); +} + +async function fetchInsights(ctx: CommandContext): Promise { + const data = await requestClientJson(ctx, "/v1/insights", { method: "GET" }) as { insights: Array<{ id: number; title: string; category: string; response: string | null }> }; + if (data.insights.length === 0) return [{ title: "(no insights)", color: "90" }]; + return data.insights.map(i => ({ + title: `[${i.category}] ${i.title}`, + detail: `ID: ${i.id}\nCategory: ${i.category}\n\n${i.response ?? "(no response)"}`, + color: "35", + })); +} + +async function fetchToday(ctx: CommandContext): Promise { + const data = await requestClientJson(ctx, "/v1/todayBrief", { method: "GET" }) as { calendar_events: unknown[]; emails: unknown[]; timezone: string }; + return [ + { title: `Calendar: ${data.calendar_events.length} events`, color: "36" }, + { title: `Emails: ${data.emails.length} messages`, color: "33" }, + { title: `Timezone: ${data.timezone}`, color: "90" }, + ]; +} + +async function fetchNow(ctx: CommandContext): Promise { + const data = await requestClientJson(ctx, "/v1/conversations?limit=5", { method: "GET" }) as { conversations: Array<{ short_summary: string | null; state: string; start_time: number }> }; + const recent = data.conversations.filter(conv => conv.start_time > Date.now() - 36000000); + if (recent.length === 0) return [{ title: "(no conversations in last 10h)", color: "90" }]; + return recent.map(conv => ({ + title: `[${new Date(conv.start_time).toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", hour12: false })}] ${conv.short_summary ?? "(processing...)"}`, + color: conv.state === "COMPLETED" ? "32" : "33", + })); +} + +async function fetchChanged(ctx: CommandContext): Promise { + const data = await requestClientJson(ctx, "/v1/changes", { method: "GET" }) as { meta: { since: number; until: number }; facts: Array<{ text: string }>; todos: Array<{ text: string }>; conversations: unknown[]; dailies: unknown[]; journals: unknown[] }; + const items: ContentItem[] = []; + items.push({ title: `Period: ${new Date(data.meta.since).toLocaleString()} → ${new Date(data.meta.until).toLocaleString()}`, color: "90" }); + for (const f of data.facts) items.push({ title: `[fact] ${f.text}`, color: "36" }); + for (const t of data.todos) items.push({ title: `[todo] ${t.text}`, color: "33" }); + if (data.conversations.length > 0) items.push({ title: `[conversations] ${data.conversations.length} changed`, color: "35" }); + if (data.dailies.length > 0) items.push({ title: `[dailies] ${data.dailies.length} changed`, color: "32" }); + if (data.journals.length > 0) items.push({ title: `[journals] ${data.journals.length} changed`, color: "90" }); + return items; +} + +async function fetchActivity(ctx: CommandContext): Promise { + const data = await requestClientJson(ctx, "/v1/activity?limit=10", { method: "GET" }) as { activities: Array<{ id: number; type: string; summary: string; timestamp: number }> }; + if (!data.activities || data.activities.length === 0) return [{ title: "(no recent activity)", color: "90" }]; + return data.activities.map(a => ({ + title: `[${a.type}] ${a.summary}`, + detail: `ID: ${a.id}\nType: ${a.type}\nTime: ${new Date(a.timestamp).toLocaleString()}`, + color: "36", + })); +} + +async function fetchLocations(ctx: CommandContext): Promise { + const data = await requestClientJson(ctx, "/v1/locations?limit=10", { method: "GET" }) as { locations: Array<{ id: number; name: string; latitude: number; longitude: number; timestamp: number }> }; + if (!data.locations || data.locations.length === 0) return [{ title: "(no locations)", color: "90" }]; + return data.locations.map(l => ({ + title: `${l.name}`, + detail: `ID: ${l.id}\nCoords: ${l.latitude}, ${l.longitude}\nTime: ${new Date(l.timestamp).toLocaleString()}`, + color: "36", + })); +} + +async function fetchPhotos(ctx: CommandContext): Promise { + const data = await requestClientJson(ctx, "/v1/photos?limit=10", { method: "GET" }) as { photos: Array<{ id: string; description: string; timestamp: number }> }; + if (!data.photos || data.photos.length === 0) return [{ title: "(no photos)", color: "90" }]; + return data.photos.map(p => ({ + title: `${p.description.slice(0, 50)}`, + detail: `ID: ${p.id}\nTime: ${new Date(p.timestamp).toLocaleString()}\n\n${p.description}`, + color: "36", + })); +} + +type Pane = "menu" | "content"; + +export async function startDashboard(): Promise { + if (!process.stdin.isTTY) { + console.error("Dashboard requires an interactive terminal (TTY)."); + process.exitCode = 1; + return; + } + + let menuIndex = 0; + let contentIndex = 0; + let contentScroll = 0; + let contentItems: ContentItem[] = []; + let expandedIndex: number | null = null; + let activePane: Pane = "menu"; + let loading = false; + + const ctx = await createCommandContext("prod"); + + const render = () => { + const cols = process.stdout.columns ?? 80; + const rows = process.stdout.rows ?? 24; + const contentMaxWidth = cols - MENU_COL_WIDTH - 4; + const maxBodyRows = rows - 4; + + process.stdout.write("\x1b[H\x1b[2J"); + + // Header + const menuHighlight = activePane === "menu" ? "\x1b[1;37m" : "\x1b[90m"; + const contentHighlight = activePane === "content" ? "\x1b[1;37m" : "\x1b[90m"; + process.stdout.write(`\u{1F41D} ${menuHighlight}Bee Dashboard\x1b[0m ${contentHighlight}│ Content\x1b[0m \x1b[90mTab switch ↑↓ navigate Enter expand q quit\x1b[0m\n`); + process.stdout.write(`\x1b[90m${"─".repeat(cols)}\x1b[0m\n`); + + // Build content lines + const contentLines: string[] = []; + if (contentItems.length === 0) { + contentLines.push("\x1b[90mPress Enter to load...\x1b[0m"); + } else { + for (let i = 0; i < contentItems.length; i++) { + const item = contentItems[i]!; + const isSelected = activePane === "content" && i === contentIndex; + const prefix = isSelected ? "\x1b[36;1m ▸ " : `\x1b[${item.color ?? "37"}m `; + contentLines.push(`${prefix}${item.title}\x1b[0m`); + + if (expandedIndex === i && item.detail) { + const detailLines = item.detail.split("\n"); + for (const dl of detailLines) { + contentLines.push(`\x1b[90m ${dl}\x1b[0m`); + } + contentLines.push(""); + } + } + } + + // Apply scroll to content + const visibleContent = contentLines.slice(contentScroll, contentScroll + maxBodyRows); + const totalRows = Math.max(MENU_ITEMS.length, visibleContent.length); + + for (let i = 0; i < Math.min(totalRows, maxBodyRows); i++) { + let menuCell = ""; + if (i < MENU_ITEMS.length) { + const item = MENU_ITEMS[i]!; + if (i === menuIndex) { + const highlight = activePane === "menu" ? "\x1b[36;1m" : "\x1b[37m"; + menuCell = `${highlight} ▸ ${item.label}\x1b[0m`; + } else { + menuCell = `\x1b[90m ${item.label}\x1b[0m`; + } + } + + const contentCell = i < visibleContent.length ? truncateVisible(visibleContent[i] ?? "", contentMaxWidth) : ""; + process.stdout.write(`${padVisible(menuCell, MENU_COL_WIDTH)} \x1b[90m│\x1b[0m ${contentCell}\n`); + } + + // Footer + process.stdout.write(`\x1b[90m${"─".repeat(cols)}\x1b[0m\n`); + if (loading) { + process.stdout.write(` \x1b[33m⟳ Loading...\x1b[0m\n`); + } else { + const paneLabel = activePane === "menu" ? "Menu" : "Content"; + const itemCount = contentItems.length > 0 ? `${contentItems.length} items` : "empty"; + const scrollInfo = contentLines.length > maxBodyRows ? ` scroll ${contentScroll + 1}/${contentLines.length}` : ""; + process.stdout.write(` \x1b[32m●\x1b[0m \x1b[90m${MENU_ITEMS[menuIndex]!.label} | ${paneLabel} | ${itemCount}${scrollInfo}\x1b[0m\n`); + } + }; + + const loadContent = async () => { + loading = true; + expandedIndex = null; + contentIndex = 0; + contentScroll = 0; + render(); + try { + contentItems = await MENU_ITEMS[menuIndex]!.fetch(ctx); + } catch (error) { + const msg = error instanceof Error ? error.message : "Unknown error"; + contentItems = [{ title: `Error: ${msg}`, color: "31" }]; + } + loading = false; + render(); + }; + + process.stdout.write("\x1b[?25l"); + render(); + + // Re-render on terminal resize + process.stdout.on("resize", () => render()); + + const stdin = process.stdin; + stdin.setRawMode(true); + stdin.resume(); + stdin.setEncoding("utf8"); + + const cleanup = () => { + stdin.setRawMode(false); + stdin.pause(); + process.stdout.removeAllListeners("resize"); + process.stdout.write("\x1b[?25h"); + process.stdout.write("\x1b[H\x1b[2J"); + }; + + stdin.on("data", async (key: string) => { + if (key === "q" || key === "\x03") { + cleanup(); + process.exit(0); + } + + // Tab - switch panes + if (key === "\t") { + activePane = activePane === "menu" ? "content" : "menu"; + render(); + return; + } + + // Escape - collapse expanded or switch to menu + if (key === "\x1b" && expandedIndex !== null) { + expandedIndex = null; + render(); + return; + } + if (key === "\x1b") { + activePane = "menu"; + render(); + return; + } + + // Arrow up + if (key === "\x1b[A" || key === "k") { + if (activePane === "menu") { + menuIndex = (menuIndex - 1 + MENU_ITEMS.length) % MENU_ITEMS.length; + } else { + if (contentIndex > 0) contentIndex--; + adjustScroll(); + } + render(); + return; + } + + // Arrow down + if (key === "\x1b[B" || key === "j") { + if (activePane === "menu") { + menuIndex = (menuIndex + 1) % MENU_ITEMS.length; + } else { + if (contentIndex < contentItems.length - 1) contentIndex++; + adjustScroll(); + } + render(); + return; + } + + // Enter + if (key === "\r" || key === "\n") { + if (activePane === "menu") { + await loadContent(); + activePane = "content"; + render(); + } else { + if (contentItems[contentIndex]?.detail) { + expandedIndex = expandedIndex === contentIndex ? null : contentIndex; + render(); + } + } + return; + } + + // Arrow right - switch to content + if (key === "\x1b[C" || key === "l") { + if (activePane === "menu" && contentItems.length > 0) { + activePane = "content"; + render(); + } + return; + } + + // Arrow left - switch to menu + if (key === "\x1b[D" || key === "h") { + activePane = "menu"; + expandedIndex = null; + render(); + return; + } + }); + + function adjustScroll() { + const rows = process.stdout.rows ?? 24; + const maxBodyRows = rows - 4; + let linesBefore = 0; + for (let i = 0; i < contentIndex; i++) { + linesBefore++; + if (expandedIndex === i && contentItems[i]?.detail) { + linesBefore += contentItems[i]!.detail!.split("\n").length + 1; + } + } + if (linesBefore < contentScroll) contentScroll = linesBefore; + if (linesBefore >= contentScroll + maxBodyRows) contentScroll = linesBefore - maxBodyRows + 1; + } +} diff --git a/sources/tui/index.ts b/sources/tui/index.ts new file mode 100644 index 0000000..4b6774a --- /dev/null +++ b/sources/tui/index.ts @@ -0,0 +1,16 @@ +export { + ansi, + shouldUseTui, + createSpinner, + renderFactsList, + renderTodosList, + renderConversationsList, + renderDevicesList, + renderSearchResults, + renderProfile, + renderStatus, + renderError, + renderDailySummary, + renderChanged, +} from "./renderer"; +export type { TuiTheme, PaginationInfo } from "./renderer"; diff --git a/sources/tui/renderer.ts b/sources/tui/renderer.ts new file mode 100644 index 0000000..efa1f31 --- /dev/null +++ b/sources/tui/renderer.ts @@ -0,0 +1,468 @@ +import type { OutputFormat } from "@/utils/format"; + +export type TuiTheme = { + primary: string; + secondary: string; + success: string; + warning: string; + error: string; + muted: string; + heading: string; +}; + +const THEME: TuiTheme = { + primary: "36", + secondary: "35", + success: "32", + warning: "33", + error: "31", + muted: "90", + heading: "1;37", +}; + +const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; +const SPINNER_FRAMES_ASCII = ["|", "/", "-", "\\"]; + +type TerminalCaps = { + color: "none" | "16" | "256" | "truecolor"; + hyperlinks: boolean; + unicode: boolean; + italic: boolean; +}; + +function detectCapabilities(): TerminalCaps { + const term = process.env["TERM"] ?? ""; + const colorterm = process.env["COLORTERM"] ?? ""; + const termProgram = process.env["TERM_PROGRAM"] ?? ""; + + // Hyperlink support (OSC 8) + const hyperlinkTerminals = [ + "iterm2", "iterm.app", + "wezterm", + "ghostty", + "windows terminal", "windowsterminal", + "vscode", + "rio", + "alacritty", + "contour", + "foot", + "kitty", + ]; + const hyperlinks = hyperlinkTerminals.some(t => + termProgram.toLowerCase().includes(t) || + term.toLowerCase().includes(t) + ) || process.env["TERM_PROGRAM_VERSION"]?.includes("WezTerm") === true; + + // Color depth + let color: TerminalCaps["color"] = "16"; + if (colorterm === "truecolor" || colorterm === "24bit") { + color = "truecolor"; + } else if (term.includes("256color") || colorterm === "256color") { + color = "256"; + } else if (term === "linux" || term === "dumb") { + color = term === "dumb" ? "none" : "16"; + } + + // macOS Terminal.app: 256 color max, no truecolor + if (termProgram === "Apple_Terminal") { + color = "256"; + } + + // Unicode support (linux console is the main exception) + const unicode = term !== "linux" && term !== "dumb"; + + // Italic support + const italic = term !== "linux" && term !== "dumb" && termProgram !== "Apple_Terminal"; + + return { color, hyperlinks, unicode, italic }; +} + +let _caps: TerminalCaps | null = null; +function getCaps(): TerminalCaps { + if (!_caps) _caps = detectCapabilities(); + return _caps; +} + +function noColorEnabled(): boolean { + return process.env["NO_COLOR"] !== undefined || + process.env["BEE_NO_COLOR"] === "1" || + getCaps().color === "none"; +} + +function c(code: string, text: string): string { + if (noColorEnabled()) return text; + return `\x1b[${code}m${text}\x1b[0m`; +} + +function dim(text: string): string { + return c(THEME.muted, text); +} + +function bold(text: string): string { + return c("1", text); +} + +function italic(text: string): string { + if (!getCaps().italic) return text; + return c("3", text); +} + +function underline(text: string): string { + return c("4", text); +} + +function strikethrough(text: string): string { + return c("9", text); +} + +function link(url: string, label?: string): string { + if (noColorEnabled()) return label ? `${label} (${url})` : url; + const display = label ?? url; + if (!getCaps().hyperlinks) { + return label ? `${c(THEME.primary, underline(display))} ${dim(`(${url})`)}` : c(THEME.primary, underline(url)); + } + return `\x1b]8;;${url}\x1b\\${c(THEME.primary, underline(display))}\x1b]8;;\x1b\\`; +} + +function bg(code: string, text: string): string { + if (noColorEnabled()) return text; + return `\x1b[${code}m${text}\x1b[0m`; +} + +function rgb(r: number, g: number, b: number, text: string): string { + if (noColorEnabled()) return text; + const caps = getCaps(); + if (caps.color === "truecolor") { + return `\x1b[38;2;${r};${g};${b}m${text}\x1b[0m`; + } + // Fallback: map to closest 256-color or 16-color ANSI + if (caps.color === "256") { + const code = rgbTo256(r, g, b); + return `\x1b[38;5;${code}m${text}\x1b[0m`; + } + // 16-color fallback: use closest basic color + return c(rgbTo16(r, g, b), text); +} + +function bgRgb(r: number, g: number, b: number, text: string): string { + if (noColorEnabled()) return text; + const caps = getCaps(); + if (caps.color === "truecolor") { + return `\x1b[48;2;${r};${g};${b}m${text}\x1b[0m`; + } + if (caps.color === "256") { + const code = rgbTo256(r, g, b); + return `\x1b[48;5;${code}m${text}\x1b[0m`; + } + return text; +} + +function rgbTo256(r: number, g: number, b: number): number { + if (r === g && g === b) { + if (r < 8) return 16; + if (r > 248) return 231; + return Math.round((r - 8) / 247 * 24) + 232; + } + return 16 + (36 * Math.round(r / 255 * 5)) + (6 * Math.round(g / 255 * 5)) + Math.round(b / 255 * 5); +} + +function rgbTo16(r: number, g: number, b: number): string { + const brightness = (r + g + b) / 3; + if (brightness < 64) return "30"; + if (r > g && r > b) return brightness > 180 ? "91" : "31"; + if (g > r && g > b) return brightness > 180 ? "92" : "32"; + if (b > r && b > g) return brightness > 180 ? "94" : "34"; + if (r > 200 && g > 200) return "93"; + if (r > 200 && b > 200) return "95"; + if (g > 200 && b > 200) return "96"; + return brightness > 180 ? "97" : "37"; +} + +function stripAnsi(s: string): string { + return s.replace(/\x1b\]8;;[^\x1b]*\x1b\\/g, "").replace(/\x1b\[[0-9;]*m/g, ""); +} + +function header(title: string, subtitle?: string): string { + const cols = process.stdout.columns ?? 80; + const sub = subtitle ? ` ${dim(subtitle)}` : ""; + const line = dim("─".repeat(Math.max(0, cols - stripAnsi(title).length - stripAnsi(sub).length - 2))); + return `${c(THEME.heading, title)}${sub} ${line}`; +} + +function footer(info?: string): string { + if (!info) return ""; + return dim(info); +} + +export const ansi = { + c, + dim, + bold, + italic, + underline, + strikethrough, + link, + bg, + rgb, + bgRgb, + stripAnsi, + noColorEnabled, + getCaps, +}; + +export function shouldUseTui(format: OutputFormat): boolean { + if (format === "json" || format === "minimal") return false; + if (process.env["BEE_NO_TUI"] === "1") return false; + if (!process.stdout.isTTY) return false; + return true; +} + +export type PaginationInfo = { + showing: number; + total?: number; + hasMore: boolean; + cursor?: string | null; +}; + +function paginationHint(info: PaginationInfo): string { + if (!info.hasMore) return ""; + const totalStr = info.total ? ` of ${info.total}` : "+"; + return dim(` ↓ showing ${info.showing}${totalStr} — use --limit N or --all for more`); +} + +export function createSpinner(message: string): { start: () => void; stop: (finalMessage?: string) => void } { + if (!process.stdout.isTTY || noColorEnabled()) { + return { + start: () => process.stderr.write(`${message}...\n`), + stop: (final?: string) => { if (final) process.stderr.write(`${final}\n`); }, + }; + } + + let frameIndex = 0; + let interval: ReturnType | null = null; + const frames = getCaps().unicode ? SPINNER_FRAMES : SPINNER_FRAMES_ASCII; + + return { + start: () => { + process.stderr.write(`\x1b[?25l`); + interval = setInterval(() => { + const frame = frames[frameIndex % frames.length]; + process.stderr.write(`\r${c(THEME.primary, frame!)} ${message}`); + frameIndex++; + }, 80); + }, + stop: (final?: string) => { + if (interval) clearInterval(interval); + process.stderr.write(`\r\x1b[2K`); + process.stderr.write(`\x1b[?25h`); + if (final) process.stderr.write(`${c(THEME.success, "✓")} ${final}\n`); + }, + }; +} + +export function renderFactsList( + facts: Array<{ id: number; text: string; tags: string[]; confirmed: boolean }>, + pagination?: PaginationInfo +): void { + const confirmed = facts.filter(f => f.confirmed); + const pending = facts.filter(f => !f.confirmed); + + console.log(header("Facts", `${facts.length} items`)); + console.log(""); + + if (confirmed.length > 0) { + console.log(c(THEME.success, ` ▸ Confirmed (${confirmed.length})`)); + for (const fact of confirmed) { + const tags = fact.tags.length > 0 ? dim(` [${fact.tags.join(", ")}]`) : ""; + console.log(` ${c(THEME.primary, fact.text)}${tags}`); + } + console.log(""); + } + + if (pending.length > 0) { + console.log(c(THEME.warning, ` ▸ Pending (${pending.length})`)); + for (const fact of pending) { + console.log(` ${dim(fact.text)}`); + } + console.log(""); + } + + if (facts.length === 0) { + console.log(dim(" (none)")); + console.log(""); + } + + if (pagination) console.log(paginationHint(pagination)); +} + +export function renderTodosList( + todos: Array<{ id: number; text: string; completed: boolean; alarm_at: number | null }>, + pagination?: PaginationInfo +): void { + const open = todos.filter(t => !t.completed); + const done = todos.filter(t => t.completed); + + console.log(header("Todos", `${open.length} open, ${done.length} done`)); + console.log(""); + + if (open.length > 0) { + for (const todo of open) { + const alarm = todo.alarm_at ? dim(" ⏰") : ""; + console.log(` ${c(THEME.primary, "○")} ${todo.text}${alarm}`); + } + console.log(""); + } + + if (done.length > 0) { + console.log(dim(` ── completed ──`)); + for (const todo of done) { + console.log(` ${dim("●")} ${dim(todo.text)}`); + } + console.log(""); + } + + if (todos.length === 0) { + console.log(dim(" (none)")); + console.log(""); + } + + if (pagination) console.log(paginationHint(pagination)); +} + +export function renderConversationsList( + conversations: Array<{ id: number; short_summary: string | null; state: string; start_time: number }>, + pagination?: PaginationInfo +): void { + console.log(header("Conversations", `${conversations.length} items`)); + console.log(""); + + if (conversations.length === 0) { + console.log(dim(" (none)")); + } else { + for (const conv of conversations) { + const date = new Date(conv.start_time).toLocaleDateString("en-CA"); + const time = new Date(conv.start_time).toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", hour12: false }); + const stateColor = conv.state === "COMPLETED" ? THEME.success : conv.state === "CAPTURING" ? THEME.warning : THEME.muted; + const summary = conv.short_summary ?? "(processing...)"; + console.log(` ${dim(`${date} ${time}`)} ${c(stateColor, conv.state.toLowerCase())} ${c(THEME.primary, summary)}`); + } + } + console.log(""); + + if (pagination) console.log(paginationHint(pagination)); +} + +export function renderDevicesList( + devices: Array<{ id: string; name: string; vendor: string; model: string; type: string; active: boolean }> +): void { + console.log(header("Devices", `${devices.length} connected`)); + console.log(""); + + if (devices.length === 0) { + console.log(dim(" (none)")); + } else { + for (const device of devices) { + const dot = device.active ? c(THEME.success, "●") : c(THEME.error, "○"); + console.log(` ${dot} ${c(THEME.primary, device.name)} ${dim(`${device.vendor} ${device.model}`)} ${c(THEME.secondary, device.type)}`); + } + } + console.log(""); +} + +export function renderSearchResults( + results: Array<{ id: string; type: string; score: number; title_snippet?: string; snippet?: string }> +): void { + console.log(header("Search", `${results.length} results`)); + console.log(""); + + if (results.length === 0) { + console.log(dim(" No results found.")); + } else { + for (const result of results) { + const scoreBar = c(THEME.success, "█".repeat(Math.min(8, Math.round(result.score * 6)))); + const title = (result.title_snippet ?? result.id).replace(/\*\*/g, ""); + console.log(` ${c(THEME.secondary, `[${result.type}]`)} ${c(THEME.primary, title)} ${scoreBar}`); + if (result.snippet) { + const clean = result.snippet.replace(/\*\*/g, "").slice(0, 90); + console.log(` ${dim(clean)}`); + } + console.log(""); + } + } +} + +export function renderProfile(profile: { id: number; first_name: string; last_name: string; timezone: string }): void { + console.log(header("Profile")); + console.log(""); + console.log(` ${dim("Name")} ${c(THEME.primary, `${profile.first_name} ${profile.last_name}`)}`); + console.log(` ${dim("ID")} ${c(THEME.primary, String(profile.id))}`); + console.log(` ${dim("Timezone")} ${c(THEME.primary, profile.timezone)}`); + console.log(""); +} + +export function renderStatus(status: { authenticated: boolean; user_id: number | null; environment: string; api_reachable: boolean; version: string }): void { + const authIcon = status.authenticated ? c(THEME.success, "●") : c(THEME.error, "○"); + const apiIcon = status.api_reachable ? c(THEME.success, "●") : c(THEME.error, "○"); + + console.log(header("Status")); + console.log(""); + console.log(` ${dim("Auth")} ${authIcon} ${status.authenticated ? "authenticated" : "not authenticated"}`); + console.log(` ${dim("User")} ${c(THEME.primary, String(status.user_id ?? "—"))}`); + console.log(` ${dim("Env")} ${c(THEME.primary, status.environment)}`); + console.log(` ${dim("API")} ${apiIcon} ${status.api_reachable ? "reachable" : "unreachable"}`); + console.log(` ${dim("Version")} ${c(THEME.primary, status.version)}`); + console.log(""); +} + +export function renderError(error: { message: string; code: number; recoverable: boolean; suggestion?: string }): void { + console.error(""); + console.error(` ${c(THEME.error, "✗")} ${error.message}`); + console.error(` ${dim(`exit ${error.code}`)}${error.recoverable ? dim(" (recoverable)") : ""}`); + if (error.suggestion) { + console.error(` ${c(THEME.success, "→")} ${error.suggestion}`); + } + console.error(""); +} + +export function renderDailySummary(daily: { id: number; short_summary: string; date_time: number }): void { + const date = new Date(daily.date_time).toLocaleDateString("en-CA"); + console.log(header("Daily Summary", date)); + console.log(""); + console.log(` ${daily.short_summary}`); + console.log(""); + console.log(footer(dim(`id: ${daily.id}`))); +} + +export function renderChanged( + meta: { since: number; until: number; updated: boolean; next_cursor: string | null }, + counts: { facts: number; todos: number; conversations: number; dailies: number; journals: number } +): void { + const since = new Date(meta.since).toLocaleString(); + const until = new Date(meta.until).toLocaleString(); + const total = counts.facts + counts.todos + counts.conversations + counts.dailies + counts.journals; + + console.log(header("Changed", `${total} updates`)); + console.log(""); + console.log(` ${dim("Period")} ${c(THEME.primary, since)} → ${c(THEME.primary, until)}`); + console.log(""); + + const items = [ + { label: "Facts", count: counts.facts, color: THEME.primary }, + { label: "Todos", count: counts.todos, color: THEME.warning }, + { label: "Conversations", count: counts.conversations, color: THEME.secondary }, + { label: "Dailies", count: counts.dailies, color: THEME.success }, + { label: "Journals", count: counts.journals, color: THEME.muted }, + ]; + + for (const item of items) { + if (item.count > 0) { + const bar = c(item.color, "█".repeat(Math.min(15, item.count))); + console.log(` ${dim(item.label.padEnd(14))} ${bar} ${dim(String(item.count))}`); + } + } + console.log(""); + + if (meta.next_cursor) { + console.log(footer(dim(`cursor: ${meta.next_cursor}`))); + } +} diff --git a/sources/utils/format.ts b/sources/utils/format.ts new file mode 100644 index 0000000..49c4b1f --- /dev/null +++ b/sources/utils/format.ts @@ -0,0 +1,52 @@ +export type OutputFormat = "json" | "text" | "minimal"; + +export function resolveOutputFormat(args: readonly string[]): { + format: OutputFormat; + args: string[]; +} { + let format: OutputFormat | null = null; + const remaining: string[] = []; + + for (let i = 0; i < args.length; i += 1) { + const arg = args[i]; + if (arg === undefined) continue; + + if (arg === "--json") { + format = "json"; + continue; + } + if (arg === "--pretty") { + format = "text"; + continue; + } + if (arg === "--minimal") { + format = "minimal"; + continue; + } + if (arg === "--format") { + const value = args[i + 1]; + if (value === "json" || value === "text" || value === "minimal") { + format = value; + i += 1; + continue; + } + } + + remaining.push(arg); + } + + if (format) { + return { format, args: remaining }; + } + + const envFormat = process.env["BEE_OUTPUT_FORMAT"]; + if (envFormat === "json" || envFormat === "text" || envFormat === "minimal") { + return { format: envFormat, args: remaining }; + } + + return { format: "text", args: remaining }; +} + +export function isJsonMode(format: OutputFormat): boolean { + return format === "json" || format === "minimal"; +} From 0211f4c66f6be21b07220a34ff7b99eeb8c69e8f Mon Sep 17 00:00:00 2001 From: Giovanni Laquidara Date: Thu, 11 Jun 2026 16:12:35 +0100 Subject: [PATCH 02/13] feat: add shell-based agent features (--describe, validate, exit codes) For agents that prefer CLI shell invocation over MCP protocol: - bee --describe: JSON blob of all commands for agent discovery - bee validate : pre-check auth + command existence without executing - Structured exit codes: 0=ok, 1=general, 2=auth, 3=args, 4=network, 5=rate-limit - BEE_OUTPUT_FORMAT=json: structured JSON errors on stderr (not usage text) Two agent paths now coexist: - MCP agents: bee mcp (native protocol, tool discovery via list_tools) - Shell agents: bee --describe + validate + exit codes + JSON stderr --- sources/commands/validate/index.ts | 48 ++++++++++++++++++++++++++++++ sources/main.ts | 47 +++++++++++++++++++++++++---- 2 files changed, 90 insertions(+), 5 deletions(-) create mode 100644 sources/commands/validate/index.ts diff --git a/sources/commands/validate/index.ts b/sources/commands/validate/index.ts new file mode 100644 index 0000000..eca5a7e --- /dev/null +++ b/sources/commands/validate/index.ts @@ -0,0 +1,48 @@ +import type { Command, CommandContext } from "@/commands/types"; +import { loadToken } from "@/secureStore"; + +const USAGE = "bee validate [subcommand] [--flags...]"; + +export const validateCommand: Command = { + name: "validate", + description: "Pre-validate a command without executing it.", + usage: USAGE, + run: async (args, context) => { + await handleValidate(args, context); + }, +}; + +let registeredCommands: readonly Command[] = []; + +export function setCommandRegistry(cmds: readonly Command[]): void { + registeredCommands = cmds; +} + +async function handleValidate( + args: readonly string[], + context: CommandContext +): Promise { + if (args.length === 0) { + console.log(JSON.stringify({ valid: false, reason: "No command specified", code: 3 })); + process.exitCode = 3; + return; + } + + const [commandName] = args; + + const command = registeredCommands.find(c => c.name === commandName); + if (!command) { + console.log(JSON.stringify({ valid: false, reason: `Unknown command: ${commandName}`, code: 3 })); + process.exitCode = 3; + return; + } + + const token = await loadToken(context.env); + if (!token) { + console.log(JSON.stringify({ valid: false, reason: "Not authenticated", code: 2 })); + process.exitCode = 2; + return; + } + + console.log(JSON.stringify({ valid: true })); +} diff --git a/sources/main.ts b/sources/main.ts index ef4ff4f..ddd17fc 100644 --- a/sources/main.ts +++ b/sources/main.ts @@ -1,4 +1,8 @@ import type { Command } from "@/commands/types"; +import { BeeError } from "@/errors"; +import { resolveOutputFormat } from "@/utils/format"; +import { loadToken } from "@/secureStore"; +import { validateCommand, setCommandRegistry } from "@/commands/validate"; import { activityCommand } from "@/commands/activity"; import { conversationsCommand } from "@/commands/conversations"; import { dailyCommand } from "@/commands/daily"; @@ -50,10 +54,13 @@ const commands = [ syncCommand, proxyCommand, todosCommand, + validateCommand, pingCommand, versionCommand, ] satisfies readonly Command[]; +setCommandRegistry(commands); + const commandIndex = new Map(); for (const command of commands) { commandIndex.set(command.name, command); @@ -112,6 +119,19 @@ async function runCli(): Promise { return; } + if (firstArg === "--describe") { + const token = await loadToken(parsed.env); + const blob = { + version: "0.7.1", + auth_status: token ? "valid" : "unauthenticated", + commands: Object.fromEntries( + commands.map(cmd => [cmd.name, { description: cmd.description, requires_auth: true }]) + ), + }; + console.log(JSON.stringify(blob, null, 2)); + return; + } + const commandName = firstArg; const command = commandIndex.get(commandName); @@ -132,13 +152,30 @@ async function runCli(): Promise { const context = await createCommandContext(parsed.env); await command.run(commandArgs, context); } catch (error) { - if (error instanceof Error) { - console.error(error.message); + const { format } = resolveOutputFormat(commandArgs); + if (error instanceof BeeError) { + if (format === "text") { + console.error(error.message); + printCommandHelp(command); + } else { + console.error(JSON.stringify({ + error: error.message, + code: error.exitCode, + recoverable: error.recoverable, + suggestion: error.suggestion, + })); + } + process.exitCode = error.exitCode; } else { - console.error("Unexpected error"); + const msg = error instanceof Error ? error.message : "Unexpected error"; + if (format === "text") { + console.error(msg); + printCommandHelp(command); + } else { + console.error(JSON.stringify({ error: msg, code: 1, recoverable: false })); + } + process.exitCode = 1; } - printCommandHelp(command); - process.exitCode = 1; } } From ba6257cab88f026f50ef11c1441d9cace3cd2f02 Mon Sep 17 00:00:00 2001 From: Giovanni Laquidara Date: Thu, 11 Jun 2026 16:17:44 +0100 Subject: [PATCH 03/13] test: add tests for agent features (format, errors, validate) - sources/utils/format.test.ts: 7 tests for resolveOutputFormat (flags, env var, precedence, defaults) - sources/errors.test.ts: 6 tests for typed error classes (exit codes, recoverable, suggestions, names) - sources/commands/validate/index.test.ts: 3 tests for validate (valid command, unknown command, no command) - Fix: validate skips token check in proxy mode Total: 313 tests passing (was 297) --- sources/commands/validate/index.test.ts | 93 +++++++++++++++++++++++++ sources/commands/validate/index.ts | 12 ++-- sources/errors.test.ts | 49 +++++++++++++ sources/utils/format.test.ts | 55 +++++++++++++++ 4 files changed, 204 insertions(+), 5 deletions(-) create mode 100644 sources/commands/validate/index.test.ts create mode 100644 sources/errors.test.ts create mode 100644 sources/utils/format.test.ts diff --git a/sources/commands/validate/index.test.ts b/sources/commands/validate/index.test.ts new file mode 100644 index 0000000..c8b9369 --- /dev/null +++ b/sources/commands/validate/index.test.ts @@ -0,0 +1,93 @@ +import { afterEach, describe, expect, it, spyOn } from "bun:test"; +import type { CommandContext } from "@/commands/types"; +import { createProxyClient } from "@/client"; +import { validateCommand, setCommandRegistry } from "./index"; + +type BunServer = ReturnType; + +const activeServers: BunServer[] = []; + +afterEach(() => { + for (const server of activeServers.splice(0, activeServers.length)) { + server.stop(true); + } +}); + +function createMockContext(server: BunServer): CommandContext { + return { + env: "prod", + client: createProxyClient("prod", { + address: `http://127.0.0.1:${server.port}`, + }), + }; +} + +describe("validate command", () => { + it("returns valid:true for known command with auth", async () => { + const server = Bun.serve({ hostname: "127.0.0.1", port: 0, fetch: () => new Response() }); + activeServers.push(server); + + setCommandRegistry([ + { name: "facts", description: "test", usage: "test", run: async () => {} }, + ]); + + const logs: string[] = []; + const logSpy = spyOn(console, "log").mockImplementation((...args) => { + logs.push(args.join(" ")); + }); + + try { + await validateCommand.run(["facts"], createMockContext(server)); + } finally { + logSpy.mockRestore(); + } + + const parsed = JSON.parse(logs.join("")); + expect(parsed.valid).toBe(true); + }); + + it("returns valid:false for unknown command", async () => { + const server = Bun.serve({ hostname: "127.0.0.1", port: 0, fetch: () => new Response() }); + activeServers.push(server); + + setCommandRegistry([ + { name: "facts", description: "test", usage: "test", run: async () => {} }, + ]); + + const logs: string[] = []; + const logSpy = spyOn(console, "log").mockImplementation((...args) => { + logs.push(args.join(" ")); + }); + + try { + await validateCommand.run(["boguscmd"], createMockContext(server)); + } finally { + logSpy.mockRestore(); + } + + const parsed = JSON.parse(logs.join("")); + expect(parsed.valid).toBe(false); + expect(parsed.reason).toContain("Unknown command"); + expect(parsed.code).toBe(3); + }); + + it("returns valid:false when no command specified", async () => { + const server = Bun.serve({ hostname: "127.0.0.1", port: 0, fetch: () => new Response() }); + activeServers.push(server); + + const logs: string[] = []; + const logSpy = spyOn(console, "log").mockImplementation((...args) => { + logs.push(args.join(" ")); + }); + + try { + await validateCommand.run([], createMockContext(server)); + } finally { + logSpy.mockRestore(); + } + + const parsed = JSON.parse(logs.join("")); + expect(parsed.valid).toBe(false); + expect(parsed.reason).toBe("No command specified"); + }); +}); diff --git a/sources/commands/validate/index.ts b/sources/commands/validate/index.ts index eca5a7e..08b12e3 100644 --- a/sources/commands/validate/index.ts +++ b/sources/commands/validate/index.ts @@ -37,11 +37,13 @@ async function handleValidate( return; } - const token = await loadToken(context.env); - if (!token) { - console.log(JSON.stringify({ valid: false, reason: "Not authenticated", code: 2 })); - process.exitCode = 2; - return; + if (!context.client.isProxy) { + const token = await loadToken(context.env); + if (!token) { + console.log(JSON.stringify({ valid: false, reason: "Not authenticated", code: 2 })); + process.exitCode = 2; + return; + } } console.log(JSON.stringify({ valid: true })); diff --git a/sources/errors.test.ts b/sources/errors.test.ts new file mode 100644 index 0000000..d6afe36 --- /dev/null +++ b/sources/errors.test.ts @@ -0,0 +1,49 @@ +import { describe, expect, it } from "bun:test"; +import { BeeError, AuthError, ValidationError, ApiError, RateLimitError } from "./errors"; + +describe("error classes", () => { + it("AuthError has exit code 2 and is recoverable", () => { + const err = new AuthError("Not logged in"); + expect(err).toBeInstanceOf(BeeError); + expect(err.exitCode).toBe(2); + expect(err.recoverable).toBe(true); + expect(err.suggestion).toBe("Run bee login"); + expect(err.message).toBe("Not logged in"); + }); + + it("AuthError accepts custom suggestion", () => { + const err = new AuthError("Token expired", "Refresh your token"); + expect(err.suggestion).toBe("Refresh your token"); + }); + + it("ValidationError has exit code 3 and is not recoverable", () => { + const err = new ValidationError("Missing --text flag"); + expect(err).toBeInstanceOf(BeeError); + expect(err.exitCode).toBe(3); + expect(err.recoverable).toBe(false); + expect(err.message).toBe("Missing --text flag"); + }); + + it("ApiError has exit code 4 and is recoverable", () => { + const err = new ApiError("Server error"); + expect(err).toBeInstanceOf(BeeError); + expect(err.exitCode).toBe(4); + expect(err.recoverable).toBe(true); + expect(err.suggestion).toBe("Retry with backoff"); + }); + + it("RateLimitError has exit code 5 and is recoverable", () => { + const err = new RateLimitError("Too many requests"); + expect(err).toBeInstanceOf(BeeError); + expect(err.exitCode).toBe(5); + expect(err.recoverable).toBe(true); + expect(err.suggestion).toBe("Wait and retry"); + }); + + it("all errors have correct name property", () => { + expect(new AuthError("x").name).toBe("AuthError"); + expect(new ValidationError("x").name).toBe("ValidationError"); + expect(new ApiError("x").name).toBe("ApiError"); + expect(new RateLimitError("x").name).toBe("RateLimitError"); + }); +}); diff --git a/sources/utils/format.test.ts b/sources/utils/format.test.ts new file mode 100644 index 0000000..c643b5d --- /dev/null +++ b/sources/utils/format.test.ts @@ -0,0 +1,55 @@ +import { describe, expect, it } from "bun:test"; +import { resolveOutputFormat } from "./format"; + +describe("resolveOutputFormat", () => { + it("defaults to text when no flag or env", () => { + const original = process.env["BEE_OUTPUT_FORMAT"]; + delete process.env["BEE_OUTPUT_FORMAT"]; + const { format, args } = resolveOutputFormat(["list", "--limit", "5"]); + expect(format).toBe("text"); + expect(args).toEqual(["list", "--limit", "5"]); + if (original) process.env["BEE_OUTPUT_FORMAT"] = original; + }); + + it("detects --json flag", () => { + const { format, args } = resolveOutputFormat(["list", "--json", "--limit", "5"]); + expect(format).toBe("json"); + expect(args).toEqual(["list", "--limit", "5"]); + }); + + it("detects --pretty flag", () => { + const { format, args } = resolveOutputFormat(["list", "--pretty"]); + expect(format).toBe("text"); + expect(args).toEqual(["list"]); + }); + + it("detects --minimal flag", () => { + const { format, args } = resolveOutputFormat(["--minimal", "list"]); + expect(format).toBe("minimal"); + expect(args).toEqual(["list"]); + }); + + it("detects --format json", () => { + const { format, args } = resolveOutputFormat(["--format", "json", "list"]); + expect(format).toBe("json"); + expect(args).toEqual(["list"]); + }); + + it("flag takes precedence over env var", () => { + const original = process.env["BEE_OUTPUT_FORMAT"]; + process.env["BEE_OUTPUT_FORMAT"] = "json"; + const { format } = resolveOutputFormat(["--pretty", "list"]); + expect(format).toBe("text"); + if (original) process.env["BEE_OUTPUT_FORMAT"] = original; + else delete process.env["BEE_OUTPUT_FORMAT"]; + }); + + it("reads BEE_OUTPUT_FORMAT env var", () => { + const original = process.env["BEE_OUTPUT_FORMAT"]; + process.env["BEE_OUTPUT_FORMAT"] = "minimal"; + const { format } = resolveOutputFormat(["list"]); + expect(format).toBe("minimal"); + if (original) process.env["BEE_OUTPUT_FORMAT"] = original; + else delete process.env["BEE_OUTPUT_FORMAT"]; + }); +}); From af9f51cef0c60581953f7290516a75ae508900f0 Mon Sep 17 00:00:00 2001 From: Giovanni Laquidara Date: Thu, 11 Jun 2026 16:22:33 +0100 Subject: [PATCH 04/13] feat(describe): add full parameter metadata from resource system --describe now outputs flag names, types (int/string/bool/boolString), positionals (with required flag), and side_effects for all 40 commands. Shell agents can construct valid command invocations without guessing. --- sources/main.ts | 53 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 50 insertions(+), 3 deletions(-) diff --git a/sources/main.ts b/sources/main.ts index ddd17fc..28ca6a7 100644 --- a/sources/main.ts +++ b/sources/main.ts @@ -120,13 +120,12 @@ async function runCli(): Promise { } if (firstArg === "--describe") { + const { RESOURCES } = await import("@/resources"); const token = await loadToken(parsed.env); const blob = { version: "0.7.1", auth_status: token ? "valid" : "unauthenticated", - commands: Object.fromEntries( - commands.map(cmd => [cmd.name, { description: cmd.description, requires_auth: true }]) - ), + commands: buildDescribeBlob(RESOURCES), }; console.log(JSON.stringify(blob, null, 2)); return; @@ -195,3 +194,51 @@ function parseGlobalArgs(args: readonly string[]): { env: Environment; args: str return { env, args: remaining }; } + +function buildDescribeBlob(resources: readonly import("@/resources/types").ResourceModule[]): Record { + const result: Record = {}; + + for (const resource of resources) { + const cliActions = resource.actions.filter(a => a.cli !== undefined); + if (cliActions.length === 0) continue; + + for (const action of cliActions) { + const cli = action.cli!; + const key = cli.subcommand + ? `${resource.cliCommand.name} ${cli.subcommand}` + : resource.cliCommand.name; + + const args: Array<{ name: string; type: string; required?: boolean }> = []; + + if (cli.positionals) { + for (const pos of cli.positionals) { + args.push({ name: pos.name, type: "positional", required: pos.required }); + } + } + + for (const flag of cli.flags) { + args.push({ name: flag.name, type: flag.kind }); + } + + const hasSideEffects = action.mcp?.name?.includes("create") || + action.mcp?.name?.includes("update") || + action.mcp?.name?.includes("delete"); + + result[key] = { + description: action.mcp?.description ?? resource.cliCommand.description, + args, + side_effects: hasSideEffects ?? false, + requires_auth: true, + }; + } + } + + // Add non-resource commands + result["validate"] = { description: "Pre-validate a command without executing", args: [{ name: "command", type: "positional", required: true }], side_effects: false, requires_auth: false }; + result["dashboard"] = { description: "Interactive TUI dashboard", args: [], side_effects: false, requires_auth: true }; + result["stream"] = { description: "Stream real-time events (SSE)", args: [{ name: "--types", type: "string" }, { name: "--json", type: "bool" }, { name: "--agent", type: "bool" }], side_effects: false, requires_auth: true }; + result["sync"] = { description: "Export data to markdown files", args: [{ name: "--output", type: "string" }, { name: "--only", type: "string" }], side_effects: false, requires_auth: true }; + result["mcp"] = { description: "Start MCP server for AI agents", args: [], side_effects: false, requires_auth: true }; + + return result; +} From 5ab2e3eacad53ab406182882ac3a4a9481fed139 Mon Sep 17 00:00:00 2001 From: Giovanni Laquidara Date: Thu, 11 Jun 2026 16:25:47 +0100 Subject: [PATCH 05/13] docs: add one-prompt agent instructions for Claude/OpenClaw/Hermes --- AGENT_PROMPT.md | 81 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 AGENT_PROMPT.md diff --git a/AGENT_PROMPT.md b/AGENT_PROMPT.md new file mode 100644 index 0000000..afbc861 --- /dev/null +++ b/AGENT_PROMPT.md @@ -0,0 +1,81 @@ +# Bee CLI Agent Prompt + +Copy-paste this prompt to any AI agent (Claude, OpenClaw, Hermes, MeshClaw) to give it autonomous access to your Bee data. + +--- + +## Full Prompt (for agents that need to install) + +``` +You have access to the Bee CLI tool for managing personal AI data (conversations, facts, todos, journals, daily summaries, search). + +## Setup + +Run these commands to install and authenticate: + +git clone https://github.com/giolaq/bee-cli.git +cd bee-cli +curl -fsSL https://bun.sh/install | bash +export PATH="$HOME/.bun/bin:$PATH" +bun install +export BEE_FORCE_FILE_STORE=1 +export BEE_OUTPUT_FORMAT=json +bun ./sources/main.ts login + +After login completes (opens browser), verify with: +bun ./sources/main.ts status --json + +## Usage + +Discover all commands and their parameters: +bun ./sources/main.ts --describe + +Pre-validate before write operations: +bun ./sources/main.ts validate + +Execute commands: +bun ./sources/main.ts [subcommand] [--flags] + +## Exit codes +- 0 = success (parse stdout as JSON) +- 1 = general error +- 2 = auth error (re-run login) +- 3 = invalid arguments (fix flags) +- 4 = network/API error (retry) +- 5 = rate limited (wait 60s) + +On error, stderr contains: {"error":"...","code":N,"recoverable":bool,"suggestion":"..."} + +## Examples + +bun ./sources/main.ts facts list --limit 5 +bun ./sources/main.ts todos list +bun ./sources/main.ts conversations list --limit 3 +bun ./sources/main.ts search --query "meeting" +bun ./sources/main.ts today +bun ./sources/main.ts changed +bun ./sources/main.ts facts create --text "Likes espresso" +bun ./sources/main.ts todos create --text "Review PR" +bun ./sources/main.ts validate facts create --text "test" + +## Protocol + +1. Run --describe once to learn available commands +2. Run validate before any create/update/delete +3. Parse stdout as JSON on exit 0 +4. On non-zero exit, read stderr for structured error JSON +``` + +--- + +## Short Prompt (for agents that already have shell access) + +``` +Set these env vars then use the bee CLI: + +export BEE_FORCE_FILE_STORE=1 BEE_OUTPUT_FORMAT=json +bun /path/to/bee-cli/sources/main.ts --describe + +That gives you all commands with parameters. Use `validate ` before writes. Exit codes: 0=ok, 2=auth, 3=args, 4=network, 5=rate-limit. Errors on stderr as JSON. +``` + From 70dfb74d0e601ffab37c59149ec8d165cf68fb27 Mon Sep 17 00:00:00 2001 From: Giovanni Laquidara Date: Thu, 11 Jun 2026 16:38:19 +0100 Subject: [PATCH 06/13] docs: consolidate agent instructions into AGENT.md, remove AGENT_PROMPT.md --- AGENT.md | 128 +++++++++++++++++++++++++++--------------------- AGENT_PROMPT.md | 81 ------------------------------ 2 files changed, 72 insertions(+), 137 deletions(-) delete mode 100644 AGENT_PROMPT.md diff --git a/AGENT.md b/AGENT.md index b5fdd6b..7851f02 100644 --- a/AGENT.md +++ b/AGENT.md @@ -1,46 +1,64 @@ -# Bee CLI Agent Instructions +# Bee CLI — Agent Instructions -Instructions for AI agents (OpenClaw, Hermes, etc.) to use bee-cli autonomously. +Instructions for AI agents to use bee-cli autonomously via shell. -## Setup +## Quick Start (copy-paste to any agent) -```bash -export BEE_OUTPUT_FORMAT=json -export BEE_FORCE_FILE_STORE=1 ``` +You have access to the Bee CLI for managing personal AI data. -Verify authentication: -```bash -bee status --json +Environment: + export PATH="$HOME/.bun/bin:$PATH" + export BEE_FORCE_FILE_STORE=1 + export BEE_OUTPUT_FORMAT=json + +Discover all commands: + bun ./sources/main.ts --describe + +Pre-validate before writes: + bun ./sources/main.ts validate + +Execute: + bun ./sources/main.ts [subcommand] [--flags] + +Exit codes: 0=success, 2=auth, 3=bad args, 4=network, 5=rate-limit +On error, stderr contains: {"error":"...","code":N,"recoverable":bool,"suggestion":"..."} ``` -If `"authenticated": false`, run `bee login` (requires human interaction once). +## Installation (if not already installed) -Discover available commands: ```bash -bee --describe +git clone https://github.com/giolaq/bee-cli.git +cd bee-cli +curl -fsSL https://bun.sh/install | bash +export PATH="$HOME/.bun/bin:$PATH" +bun install +export BEE_FORCE_FILE_STORE=1 +export BEE_OUTPUT_FORMAT=json +bun ./sources/main.ts login ``` -## Usage Pattern - -1. Set `BEE_OUTPUT_FORMAT=json` in your environment (all output becomes JSON). -2. Call `bee validate [subcommand] [--flags]` before mutating commands to pre-check. -3. Read stdout as JSON. All commands return structured data. -4. Check exit code after every call: - - `0` = success, process the JSON response - - `2` = auth error, run `bee login` or refresh token - - `3` = invalid arguments, fix the flags and retry - - `4` = API/network error, retry with exponential backoff - - `5` = rate limited, wait and retry - - `1` = general error, report to user -5. On non-zero exit, read stderr for structured error JSON: - ```json - {"error": "message", "code": 3, "recoverable": false, "suggestion": "..."} - ``` +## Protocol + +1. Run `--describe` once to learn available commands and their parameters +2. Run `validate ` before any create/update/delete +3. Parse stdout as JSON on exit code 0 +4. On non-zero exit, read stderr for structured error JSON + +## Exit Codes + +| Code | Meaning | Agent Action | +|------|---------|-------------| +| 0 | Success | Parse stdout JSON | +| 1 | General error | Report to user | +| 2 | Auth error | Run `bee login` | +| 3 | Invalid arguments | Fix flags, retry | +| 4 | Network/API error | Retry with backoff | +| 5 | Rate limited | Wait 60s, retry | ## Commands -### Read operations (no side effects) +### Read (no side effects) | Command | Description | |---------|-------------| @@ -48,30 +66,32 @@ bee --describe | `bee facts get ` | Get a single fact | | `bee todos list [--limit N] [--cursor C]` | List todos | | `bee todos get ` | Get a single todo | -| `bee conversations list [--limit N] [--cursor C] [--bookmarked]` | List conversations | +| `bee conversations list [--limit N] [--cursor C]` | List conversations | | `bee conversations get ` | Get conversation detail | +| `bee conversations transcript ` | Get full transcript | | `bee daily list [--limit N] [--cursor C]` | List daily summaries | | `bee daily get ` | Get daily summary detail | | `bee journals list [--limit N] [--cursor C]` | List journal entries | -| `bee search --query [--type conversations\|emails\|calendar] [--neural]` | Search across data | -| `bee today` | Today's calendar + emails brief | +| `bee journals get ` | Get a journal entry | +| `bee search --query ` | Search across data | +| `bee today` | Today's brief | | `bee now` | Recent conversations (last 10 hours) | | `bee changed [--cursor C]` | Changes since last check | -| `bee insights list [--category C]` | List insights | -| `bee me` | User profile | -| `bee status` | Auth and connection status | | `bee activity [--limit N]` | Recent activity | +| `bee insights list [--limit N]` | AI insights | | `bee locations [--limit N]` | Location history | -| `bee photos [--limit N]` | Photos with AI descriptions | +| `bee photos [--limit N]` | Photos with descriptions | +| `bee me` | User profile | +| `bee status --json` | Auth and connection status | -### Write operations (have side effects) +### Write (validate first) | Command | Description | |---------|-------------| | `bee facts create --text ` | Create a fact | -| `bee facts update --text [--confirmed true\|false]` | Update a fact | +| `bee facts update --text ` | Update a fact | | `bee facts delete ` | Delete a fact | -| `bee todos create --text [--priority N] [--alarm-at ]` | Create a todo | +| `bee todos create --text ` | Create a todo | | `bee todos update [--text T] [--completed true\|false]` | Update a todo | | `bee todos delete ` | Delete a todo | @@ -79,34 +99,30 @@ bee --describe | Command | Description | |---------|-------------| -| `bee --describe` | Full command schema (JSON blob for discovery) | -| `bee validate [args]` | Pre-validate without executing | -| `bee status --json` | Structured auth/connection status | -| `bee sync --output [--only facts\|todos\|daily\|conversations]` | Export to markdown files | -| `bee stream [--types T] [--json\|--agent]` | Real-time event stream (SSE) | +| `bee --describe` | Full command schema with parameters (JSON) | +| `bee validate ` | Pre-validate without executing | +| `bee sync --output ` | Export to markdown files | +| `bee stream [--json]` | Real-time event stream (SSE) | +| `bee mcp` | Start MCP server (alternative agent interface) | +| `bee dashboard` | Interactive TUI (humans only) | ## Pagination List commands return `next_cursor` in the response. Pass it back: ```bash -bee facts list --cursor "v1-1779315328643-10583865" +bee facts list --cursor "cursor_value_here" ``` -## Output Modes - -| Flag | Effect | -|------|--------| -| (none with `BEE_OUTPUT_FORMAT=json`) | JSON output (agent default) | -| `--pretty` | Human-readable markdown | -| `--minimal` | Compact JSON (strips timezone, no indentation) | -| `--json` | Explicit JSON (same as env var) | - ## Error Recovery ``` -Exit 2 (auth) → bee login (needs human), then retry +Exit 2 (auth) → run bee login (needs human), then retry Exit 3 (args) → fix command flags, retry immediately -Exit 4 (network) → exponential backoff: 1s, 2s, 4s, 8s, max 3 retries +Exit 4 (network) → exponential backoff: 1s, 2s, 4s, max 3 retries Exit 5 (rate) → wait 60s, then retry Exit 1 (general) → log and report to user ``` + +## Alternative: MCP Protocol + +For agents that support MCP natively, use `bee mcp` to start an MCP server instead of shell commands. MCP provides richer tool schemas and structured responses at the protocol level. diff --git a/AGENT_PROMPT.md b/AGENT_PROMPT.md deleted file mode 100644 index afbc861..0000000 --- a/AGENT_PROMPT.md +++ /dev/null @@ -1,81 +0,0 @@ -# Bee CLI Agent Prompt - -Copy-paste this prompt to any AI agent (Claude, OpenClaw, Hermes, MeshClaw) to give it autonomous access to your Bee data. - ---- - -## Full Prompt (for agents that need to install) - -``` -You have access to the Bee CLI tool for managing personal AI data (conversations, facts, todos, journals, daily summaries, search). - -## Setup - -Run these commands to install and authenticate: - -git clone https://github.com/giolaq/bee-cli.git -cd bee-cli -curl -fsSL https://bun.sh/install | bash -export PATH="$HOME/.bun/bin:$PATH" -bun install -export BEE_FORCE_FILE_STORE=1 -export BEE_OUTPUT_FORMAT=json -bun ./sources/main.ts login - -After login completes (opens browser), verify with: -bun ./sources/main.ts status --json - -## Usage - -Discover all commands and their parameters: -bun ./sources/main.ts --describe - -Pre-validate before write operations: -bun ./sources/main.ts validate - -Execute commands: -bun ./sources/main.ts [subcommand] [--flags] - -## Exit codes -- 0 = success (parse stdout as JSON) -- 1 = general error -- 2 = auth error (re-run login) -- 3 = invalid arguments (fix flags) -- 4 = network/API error (retry) -- 5 = rate limited (wait 60s) - -On error, stderr contains: {"error":"...","code":N,"recoverable":bool,"suggestion":"..."} - -## Examples - -bun ./sources/main.ts facts list --limit 5 -bun ./sources/main.ts todos list -bun ./sources/main.ts conversations list --limit 3 -bun ./sources/main.ts search --query "meeting" -bun ./sources/main.ts today -bun ./sources/main.ts changed -bun ./sources/main.ts facts create --text "Likes espresso" -bun ./sources/main.ts todos create --text "Review PR" -bun ./sources/main.ts validate facts create --text "test" - -## Protocol - -1. Run --describe once to learn available commands -2. Run validate before any create/update/delete -3. Parse stdout as JSON on exit 0 -4. On non-zero exit, read stderr for structured error JSON -``` - ---- - -## Short Prompt (for agents that already have shell access) - -``` -Set these env vars then use the bee CLI: - -export BEE_FORCE_FILE_STORE=1 BEE_OUTPUT_FORMAT=json -bun /path/to/bee-cli/sources/main.ts --describe - -That gives you all commands with parameters. Use `validate ` before writes. Exit codes: 0=ok, 2=auth, 3=args, 4=network, 5=rate-limit. Errors on stderr as JSON. -``` - From 6fba762752c2f771b1a736253bcdb8f9a0547093 Mon Sep 17 00:00:00 2001 From: Giovanni Laquidara Date: Thu, 11 Jun 2026 16:43:10 +0100 Subject: [PATCH 07/13] docs: move agent instructions to docs/AGENT_USAGE.md --- AGENT.md => docs/AGENT_USAGE.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename AGENT.md => docs/AGENT_USAGE.md (100%) diff --git a/AGENT.md b/docs/AGENT_USAGE.md similarity index 100% rename from AGENT.md rename to docs/AGENT_USAGE.md From 76184ce9d504a59ed2e086a0b4b8e56a66705d3a Mon Sep 17 00:00:00 2001 From: Giovanni Laquidara Date: Thu, 11 Jun 2026 16:44:49 +0100 Subject: [PATCH 08/13] docs(readme): add shell-based agent section linking to docs/AGENT_USAGE.md --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index 8481f8c..e1ab2bc 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,21 @@ bee mcp serve-http [--port N] # local HTTP, 127.0.0.1, bearer-token auth Every read, search, and manage capability below is exposed as an MCP tool (`bee_search`, `bee_list_facts`, `bee_get_daily_summary`, …), so the CLI and your assistant work from the same data. +## Use Bee with shell-based agents + +For AI agents that prefer shell invocation over MCP (OpenClaw, Hermes, custom agents), the CLI provides structured JSON output, typed exit codes, command discovery, and pre-validation: + +```bash +export BEE_FORCE_FILE_STORE=1 BEE_OUTPUT_FORMAT=json +bee --describe # discover all commands + parameters as JSON +bee validate facts list # pre-check before executing +bee facts list # structured JSON on stdout, errors on stderr +``` + +Exit codes: `0` success, `2` auth, `3` bad args, `4` network, `5` rate-limited. + +See **[docs/AGENT_USAGE.md](docs/AGENT_USAGE.md)** for the full agent protocol, command reference, and copy-paste prompts. + ## Installation Install from npm: From afe616daf3d8f6ebe026da06086198a28e11cb11 Mon Sep 17 00:00:00 2001 From: Giovanni Laquidara Date: Thu, 11 Jun 2026 16:47:16 +0100 Subject: [PATCH 09/13] fix: update repo URL to bee-computer/bee-cli --- docs/AGENT_USAGE.md | 2 +- docs/OPENCLAW_SETUP.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/AGENT_USAGE.md b/docs/AGENT_USAGE.md index 7851f02..4eabc34 100644 --- a/docs/AGENT_USAGE.md +++ b/docs/AGENT_USAGE.md @@ -28,7 +28,7 @@ On error, stderr contains: {"error":"...","code":N,"recoverable":bool,"suggestio ## Installation (if not already installed) ```bash -git clone https://github.com/giolaq/bee-cli.git +git clone https://github.com/bee-computer/bee-cli.git cd bee-cli curl -fsSL https://bun.sh/install | bash export PATH="$HOME/.bun/bin:$PATH" diff --git a/docs/OPENCLAW_SETUP.md b/docs/OPENCLAW_SETUP.md index 4498a05..57974d9 100644 --- a/docs/OPENCLAW_SETUP.md +++ b/docs/OPENCLAW_SETUP.md @@ -23,7 +23,7 @@ npm install -g @beeai/cli ### Option B: From source ```bash -git clone https://github.com/giolaq/bee-cli.git +git clone https://github.com/bee-computer/bee-cli.git cd bee-cli bun install bun run build @@ -33,7 +33,7 @@ bun run build ### Option C: Direct with Bun (development) ```bash -git clone https://github.com/giolaq/bee-cli.git +git clone https://github.com/bee-computer/bee-cli.git cd bee-cli bun install # Run directly: From bd884c4e29b6cfa60c31f39309129149f58d5f3d Mon Sep 17 00:00:00 2001 From: Giovanni Laquidara Date: Thu, 11 Jun 2026 16:50:43 +0100 Subject: [PATCH 10/13] docs: remove BEE_FORCE_FILE_STORE (dev-only), use bee instead of bun ./sources/main.ts --- README.md | 6 +++--- docs/AGENT_USAGE.md | 28 +++++++++++++++------------- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index e1ab2bc..ee8839a 100644 --- a/README.md +++ b/README.md @@ -63,12 +63,12 @@ bee mcp serve-http [--port N] # local HTTP, 127.0.0.1, bearer-token auth Every read, search, and manage capability below is exposed as an MCP tool (`bee_search`, `bee_list_facts`, `bee_get_daily_summary`, …), so the CLI and your assistant work from the same data. -## Use Bee with shell-based agents +## Use Bee with AI Agents (CLI) -For AI agents that prefer shell invocation over MCP (OpenClaw, Hermes, custom agents), the CLI provides structured JSON output, typed exit codes, command discovery, and pre-validation: +If your prefer CLI for your Agent over MCP, the CLI provides structured JSON output, typed exit codes, command discovery, and pre-validation: ```bash -export BEE_FORCE_FILE_STORE=1 BEE_OUTPUT_FORMAT=json +export BEE_OUTPUT_FORMAT=json bee --describe # discover all commands + parameters as JSON bee validate facts list # pre-check before executing bee facts list # structured JSON on stdout, errors on stderr diff --git a/docs/AGENT_USAGE.md b/docs/AGENT_USAGE.md index 4eabc34..574c44b 100644 --- a/docs/AGENT_USAGE.md +++ b/docs/AGENT_USAGE.md @@ -8,40 +8,42 @@ Instructions for AI agents to use bee-cli autonomously via shell. You have access to the Bee CLI for managing personal AI data. Environment: - export PATH="$HOME/.bun/bin:$PATH" - export BEE_FORCE_FILE_STORE=1 export BEE_OUTPUT_FORMAT=json Discover all commands: - bun ./sources/main.ts --describe + bee --describe Pre-validate before writes: - bun ./sources/main.ts validate + bee validate Execute: - bun ./sources/main.ts [subcommand] [--flags] + bee [subcommand] [--flags] Exit codes: 0=success, 2=auth, 3=bad args, 4=network, 5=rate-limit On error, stderr contains: {"error":"...","code":N,"recoverable":bool,"suggestion":"..."} ``` -## Installation (if not already installed) +## Installation + +```bash +npm install -g @beeai/cli +bee login +``` + +Or from source: ```bash git clone https://github.com/bee-computer/bee-cli.git cd bee-cli -curl -fsSL https://bun.sh/install | bash -export PATH="$HOME/.bun/bin:$PATH" bun install -export BEE_FORCE_FILE_STORE=1 -export BEE_OUTPUT_FORMAT=json -bun ./sources/main.ts login +bun run build +./dist/bee login ``` ## Protocol -1. Run `--describe` once to learn available commands and their parameters -2. Run `validate ` before any create/update/delete +1. Run `bee --describe` once to learn available commands and their parameters +2. Run `bee validate ` before any create/update/delete 3. Parse stdout as JSON on exit code 0 4. On non-zero exit, read stderr for structured error JSON From 71cc57580cc5ad83375d43ec1551596bd2e826dd Mon Sep 17 00:00:00 2001 From: Giovanni Laquidara Date: Thu, 11 Jun 2026 17:01:57 +0100 Subject: [PATCH 11/13] chore: remove internal research and setup docs --- docs/OPENCLAW_SETUP.md | 290 ---------------------------------- docs/TUI_PATTERNS_RESEARCH.md | 165 ------------------- 2 files changed, 455 deletions(-) delete mode 100644 docs/OPENCLAW_SETUP.md delete mode 100644 docs/TUI_PATTERNS_RESEARCH.md diff --git a/docs/OPENCLAW_SETUP.md b/docs/OPENCLAW_SETUP.md deleted file mode 100644 index 57974d9..0000000 --- a/docs/OPENCLAW_SETUP.md +++ /dev/null @@ -1,290 +0,0 @@ -# Running Bee CLI with OpenClaw - -This guide explains how to set up bee-cli for use as a tool by an OpenClaw agent, enabling autonomous access to your Bee personal AI data (conversations, facts, todos, health, calendar, emails, and more). - ---- - -## Prerequisites - -- **Node.js 18+** or **Bun 1.0+** installed -- A **Bee account** (sign up at bee.computer) -- **OpenClaw** configured and running - ---- - -## 1. Install Bee CLI - -### Option A: From npm (recommended) - -```bash -npm install -g @beeai/cli -``` - -### Option B: From source - -```bash -git clone https://github.com/bee-computer/bee-cli.git -cd bee-cli -bun install -bun run build -# Binary at ./dist/bee -``` - -### Option C: Direct with Bun (development) - -```bash -git clone https://github.com/bee-computer/bee-cli.git -cd bee-cli -bun install -# Run directly: -bun ./sources/main.ts -``` - ---- - -## 2. Authenticate - -Bee CLI requires a one-time human login to obtain an API token: - -```bash -bee login -``` - -This opens a browser for pairing. Follow the prompts. Once complete, the token is stored locally at `~/.bee/token-prod`. - -Verify authentication: - -```bash -bee status -``` - -Expected output: -``` -API: production (https://app-api-developer.ce.bee.amazon.dev/) -Token: eyJ4...Xm2k -Verified as Giovanni Laquidara (id 33070). -``` - -### Headless environments - -If running in a CI/headless environment, copy the token file from an authenticated machine: - -```bash -# On authenticated machine: -cat ~/.bee/token-prod - -# On headless machine: -mkdir -p ~/.bee && chmod 700 ~/.bee -echo "YOUR_TOKEN_HERE" > ~/.bee/token-prod -chmod 600 ~/.bee/token-prod -export BEE_FORCE_FILE_STORE=1 -``` - ---- - -## 3. Configure for Agent Mode - -Set these environment variables in your OpenClaw agent configuration: - -```bash -export BEE_OUTPUT_FORMAT=json # All output as structured JSON -export BEE_FORCE_FILE_STORE=1 # Use file-based token (no keychain dependency) -``` - -These ensure: -- Every command returns parseable JSON on stdout -- Errors return structured JSON on stderr -- No interactive prompts or UI formatting - ---- - -## 4. OpenClaw Configuration - -### Tool Definition - -Add bee-cli as a tool in your OpenClaw agent's configuration. The tool wraps shell execution of `bee` commands. - -```yaml -# openclaw-agent.yaml -tools: - - name: bee - description: | - Access the user's Bee personal AI data. Manages conversations, facts, - todos, journals, health data, calendar, emails, and more. - - SETUP: Run `bee --describe` first to discover all available commands. - Run `bee validate [args]` before mutating operations. - - EXIT CODES: - 0 = success (read stdout as JSON) - 2 = auth error (suggest re-login) - 3 = invalid arguments (fix and retry) - 4 = network/API error (retry with backoff) - 5 = rate limited (wait 60s) - type: shell - command: bee - env: - BEE_OUTPUT_FORMAT: json - BEE_FORCE_FILE_STORE: "1" -``` - -### Agent System Prompt - -Include this in your agent's system prompt so it knows how to use bee-cli: - -``` -You have access to the `bee` CLI tool which manages the user's personal AI data. - -## Discovery -Run `bee --describe` to get a JSON schema of all available commands, their parameters, -and whether they have side effects. - -## Usage Protocol -1. Before any write operation, run: bee validate [args] -2. Execute: bee [args] -3. Read stdout (JSON). Check exit code. -4. On error, read stderr for: {"error": "...", "code": N, "recoverable": bool, "suggestion": "..."} - -## Exit Code Handling -- 0: Success. Parse stdout JSON. -- 2: Auth expired. Tell user to run `bee login`. -- 3: Bad arguments. Fix flags and retry immediately. -- 4: Network/API error. Retry up to 3 times with backoff (1s, 2s, 4s). -- 5: Rate limited. Wait 60 seconds, then retry. -- 1: General error. Report to user. - -## Key Commands -- bee facts list/create/update/delete — personal knowledge base -- bee todos list/create/update/delete — task management -- bee conversations list/get — recorded conversations -- bee search --query "..." — semantic search across all data -- bee changed — what's new since last check -- bee today — today's calendar + email brief -- bee now — recent conversations (last 10 hours) -- bee daily list/get — daily narrative summaries -- bee journals list — voice journal entries -- bee health heart-rate|steps|sleep|alerts — health data -- bee calendar list — calendar events -- bee emails list — email inbox -- bee sync --output ./data — export everything to markdown files - -## Pagination -List commands may return "next_cursor" in the response. -Pass it back: bee list --cursor "cursor_value" - -## Important -- Always use `bee validate` before create/update/delete operations -- The `bee stream` command is for real-time SSE events (long-running) -- The `bee sync` command writes files to disk (specify --output directory) -``` - ---- - -## 5. Verify Agent Setup - -Run these commands to confirm everything works: - -```bash -# 1. Check auth -bee status --json -# Expected: {"authenticated":true,"user_id":...,"api_reachable":true} - -# 2. Discover commands -bee --describe | head -20 -# Expected: JSON blob with version, auth_status, commands object - -# 3. Validate a command -bee validate facts list --limit 5 -# Expected: {"valid":true} - -# 4. Execute a command -bee facts list --limit 2 -# Expected: {"facts":[...],"next_cursor":...,"timezone":"..."} - -# 5. Test error handling -bee facts create -# Expected exit 3, stderr: {"error":"Missing fact text...","code":3,...} -``` - ---- - -## 6. Example Agent Session - -Here's what a typical OpenClaw agent session looks like using bee-cli: - -``` -Agent: I'll check what the user has been up to today. - -[Executes: bee today] -→ {"calendar_events":[],"emails":[],"timezone":"America/Los_Angeles"} - -Agent: No calendar or emails today. Let me check recent conversations. - -[Executes: bee now] -→ {"since":1779275504,"until":1779311504,"timezone":"America/Los_Angeles","conversations":[]} - -Agent: No recent conversations. Let me check what changed since last time. - -[Executes: bee changed] -→ {"meta":{"next_cursor":"v1-...","updated":true},"facts":[{"id":10580853,"text":"I dance bachata",...}],"todos":[],...} - -Agent: I see a new fact was recorded. The user likes to dance bachata. - Let me search for related conversations. - -[Executes: bee validate search --query "bachata"] -→ {"valid":true} - -[Executes: bee search --query "bachata"] -→ {"results":[...],"timezone":"America/Los_Angeles"} - -Agent: Found a conversation about dancing. Let me create a todo reminder. - -[Executes: bee validate todos create --text "Look up bachata classes nearby"] -→ {"valid":true} - -[Executes: bee todos create --text "Look up bachata classes nearby"] -→ {"id":27571800,"text":"Look up bachata classes nearby","completed":false,...} - -Agent: Done! Created a todo to look up bachata classes. -``` - ---- - -## 7. Troubleshooting - -### "Not authenticated" (exit code 2) - -The token has expired or is missing. A human needs to run `bee login` once to re-authenticate. - -### "Request failed with status 500" (exit code 4) - -The Bee API is having issues. Retry with backoff. If persistent, the feature may not be enabled for this account (e.g., calendar/email require linking a provider in the Bee app). - -### "--limit must be a positive integer" (exit code 3) - -The agent sent invalid arguments. Check the `--describe` output for correct parameter types. - -### Commands returning 404 - -Some endpoints (contacts, photos, screen, checkins, health, products) require the Bee Pioneer wearable to be connected and actively recording. If the user doesn't have these features enabled, the API returns 404. - -### Token storage - -Tokens are stored at: -- `~/.bee/token-prod` (production) -- `~/.bee/token-staging` (staging) - -Ensure the directory has correct permissions: `chmod 700 ~/.bee` - ---- - -## 8. Reference - -| Resource | Location | -|----------|----------| -| Full agent instructions | `AGENT.md` in repo root | -| Command schema | `bee --describe` (runtime) | -| Validation | `bee validate [args]` | -| Source code | `sources/commands/*/index.ts` | -| Mock server (for testing) | `bun ./mock-server.ts` | -| API endpoint comparison | See `API_ENDPOINTS_COMPARISON.md` (if generated) | diff --git a/docs/TUI_PATTERNS_RESEARCH.md b/docs/TUI_PATTERNS_RESEARCH.md deleted file mode 100644 index 4aca300..0000000 --- a/docs/TUI_PATTERNS_RESEARCH.md +++ /dev/null @@ -1,165 +0,0 @@ -# Plan: TUI Research Findings — Design Patterns for Bee CLI - -## Context - -Feedback received: research what makes TUIs great in (1) frontier coding agents (Gemini CLI, Claude Code, OpenCode, Kiro) and (2) SaaS CLIs closer to bee-cli's scope (Stripe, 1Password, Tailscale, gh, Charm tools). The coding agents are heavier workbenches; the SaaS CLIs are closer to what bee-cli should be. - -**Goal:** Research only. Compile findings into a reference document for future TUI improvements. No code changes. - ---- - -## Research Results - -### Category 1: Frontier Coding Agent TUIs - -Researched: Claude Code, OpenCode (OpenTUI), Gemini CLI, Kiro - -#### Key Patterns - -**1. Adaptive Output Detection** -All tools detect `process.stdout.isTTY` to decide between rich TUI and plain output. Support multiple formats (JSON, text, minimal) for piping. This is exactly what bee-cli already does with `shouldUseTui()`. - -**2. Box-Based Panels (not full-screen)** -Rather than full-screen frameworks, these tools use Unicode box rendering (╭─╮ │ ╰─╯) for self-contained panels. Panels are independent units sized to terminal width. Avoids heavyweight TUI libraries. - -**3. Semantic ANSI Color Theme** -Consistent palette mapped to developer intent: -- Cyan (36): content, data values -- Green (32): success, active, confirmed -- Yellow (33): pending, in-progress, warnings -- Red (31): errors, inactive -- Gray (90): metadata, secondary info -- Bold white (1;37): section headings - -Uses raw ANSI codes, not external libraries. - -**4. In-Place Progress (no scrolling)** -Braille spinners (⠋⠙⠹⠸) at 80ms. Multi-task progress uses cursor movement (`\x1b[nA`) to overwrite previous lines rather than scrolling the terminal. - -**5. Smart Truncation** -Strip ANSI before measuring width. Truncate mid-line preserving color codes. Virtual scrolling (track offset, render visible window only). - -**6. Two-Pane Dashboard** -Fixed narrow left pane (navigation), scrollable right pane (content). Expand items for detail. Vim keys + arrow keys. - -**7. Error Boxes with Metadata** -Error display includes: icon (✗), message, error code, recoverability flag, and actionable suggestion in a dedicated box. - -**8. What Makes Them "Good"** -- Zero dependencies (raw ANSI) -- Graceful degradation (works in pipes, CI) -- Responsive to terminal width -- Information density over flashiness -- Fast redraws via cursor positioning -- Dashboard is optional; commands work standalone - ---- - -### Category 2: SaaS CLI TUI Patterns - -Researched: Stripe CLI, GitHub CLI (gh), 1Password (op), Tailscale, Charm tools (lipgloss, bubbletea, gum) - -#### Key Patterns - -**1. One-Shot Default, Interactive Optional** -Default behavior: print and exit. Users expect to pipe output. Interactive mode only when explicitly requested (`--interactive`) or when piping is unavailable. Never force modal UI for data commands. - -**2. Conservative Color Usage** -- Green: success/positive only -- Red: errors only -- Cyan/blue: interactive prompts -- Dim/faint: timestamps, IDs, supplementary info -- Respect `NO_COLOR` env var -- Auto-downsample to 256/16 color terminals - -**3. Data Display Strategy** -- Lists: compact tables with consistent column alignment -- Large datasets: limit by default (e.g., 10 items), show `↓ 47 more` with `--all` flag -- Key-value: left-align keys, dim metadata -- Never paginate interactively for data commands - -**4. Flags for Output Control** -- `--json` for machine-readable -- `--limit N` for row count -- `--wide` for expanded columns -- `--no-limit` / `--all` for full dataset -- `--depth N` for nested data - -**5. Error Display** -- Print to stderr (not stdout) -- One-line format: `Error: [brief message]` -- Add `--help` suggestion on ambiguous commands -- Structured exit codes for automation - -**6. Progress/Status** -- Quick ops (<500ms): no indicator needed -- Long ops: simple spinner -- Streaming: timestamp-prefixed log lines (Stripe webhook listener model) - -**7. Lightweight Feel** -- Minimize startup time (no heavy frameworks for simple commands) -- Dense output by default, `--verbose` for extra -- Borders/boxes used sparingly (one per logical section, not every element) -- Fast commands should feel instant - ---- - -## Comparison: Bee CLI Current State vs Best Practices - -| Pattern | Bee CLI Current | Best Practice | Gap | -|---------|----------------|---------------|-----| -| TTY detection | ✅ `shouldUseTui()` | Adaptive | None | -| Color scheme | ✅ Semantic ANSI | Consistent palette | None | -| Box rendering | ✅ Unicode borders | Self-contained panels | None | -| One-shot default | ⚠️ Dashboard is separate cmd | One-shot for data cmds | Minor | -| `NO_COLOR` support | ❌ Missing | Respect env var | Gap | -| Data overflow | ✅ Truncation + scroll | `--all` / count indicator | Could add | -| Error on stderr | ✅ In JSON mode | Always stderr for errors | None | -| Progress spinners | ❌ Not in commands | Spinner for API calls | Gap | -| `--wide` flag | ❌ Missing | Expanded output option | Gap | -| Startup speed | ✅ Fast (Bun) | <100ms for simple cmds | None | -| Vim + arrow keys | ✅ In dashboard | Dual bindings | None | -| Borders sparingly | ⚠️ Box around everything | One per section max | Minor | - ---- - -## Top Recommendations (for future implementation) - -### High Priority (matches SaaS CLI patterns) -1. **Add `NO_COLOR` env var support** — disable all ANSI when set (standard: https://no-color.org/) -2. **Add item count indicator** — when output is limited, show "showing 10 of 47, use --all for full list" -3. **Add loading spinners** — for API calls taking >500ms, show inline spinner -4. **Lighten the box usage** — in one-shot TUI mode, use boxes only for the outer container, not every sub-section - -### Medium Priority (good DX) -5. **Add `--wide` flag** — show all fields without truncation (alternative to `--json`) -6. **Add streaming progress for sync** — show real-time progress during `bee sync` with spinner per target -7. **Respect terminal resize** — listen to SIGWINCH in dashboard mode - -### Low Priority (frontier agent patterns, heavier than needed) -8. **In-place multi-task progress** — cursor movement to update multiple progress lines (for sync) -9. **Syntax highlighting for conversation transcripts** — tree-sitter for code blocks in conversations - ---- - -## Key Insight from the Feedback - -> "Those are full workbenches built to invoke other tools, so they're heavier than what you're building. Worth comparing against SaaS CLIs too. Lean a bit slimmer than the frontier coding agents." - -**Translation for bee-cli:** -- Don't build toward OpenCode/Claude Code's full-screen TUI complexity -- The current two-pane dashboard is fine as an *optional* mode -- Default commands should feel like `gh` or `stripe`: fast, one-shot, pipe-friendly -- The box renderer (`sources/tui/renderer.ts`) is the right weight for one-shot output -- The dashboard (`sources/tui/dashboard.ts`) is the right weight for interactive exploration -- Don't add bubbletea/lipgloss dependencies — raw ANSI is the correct choice for this project size - ---- - -## Deliverable - -Write these findings to `docs/TUI_PATTERNS_RESEARCH.md` in the project. - -## Verification - -N/A — this is a research document, no code changes needed. From 4d7d5856ebbd6834eafe89fc8cfb40fd34c8d743 Mon Sep 17 00:00:00 2001 From: Giovanni Laquidara Date: Thu, 11 Jun 2026 17:41:49 +0100 Subject: [PATCH 12/13] fix: remove Handlebars dep, use ValidationError in all commands - Replace Handlebars with 3-line renderTemplate() regex in stream command - Remove handlebars from package.json dependencies - Use ValidationError (exit code 3) instead of generic Error in: changed (--cursor, unknown option, unexpected args) login (--token, --proxy, --token-stdin, unknown option, unexpected args) mcp (missing subcommand) --- package.json | 1 - sources/commands/changed/index.ts | 7 ++++--- sources/commands/login/index.ts | 13 +++++++------ sources/commands/mcp/index.ts | 3 ++- sources/commands/stream/index.ts | 31 ++++++++++++++++--------------- 5 files changed, 29 insertions(+), 26 deletions(-) diff --git a/package.json b/package.json index ce1bd89..6d0fd3e 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,6 @@ }, "dependencies": { "date-fns": "^4.1.0", - "handlebars": "^4.7.8", "qrcode": "^1.5.4", "tweetnacl": "^1.0.3" }, diff --git a/sources/commands/changed/index.ts b/sources/commands/changed/index.ts index bb568bc..b24132a 100644 --- a/sources/commands/changed/index.ts +++ b/sources/commands/changed/index.ts @@ -1,4 +1,5 @@ import type { Command, CommandContext } from "@/commands/types"; +import { ValidationError } from "@/errors"; import { printJson, requestClientJson } from "@/client/clientApi"; import { formatDateValue, @@ -103,7 +104,7 @@ function parseChangedArgs(args: readonly string[]): ChangedOptions { if (arg === "--cursor") { const value = args[i + 1]; if (value === undefined) { - throw new Error("--cursor requires a value"); + throw new ValidationError("--cursor requires a value"); } cursor = value; i += 1; @@ -111,14 +112,14 @@ function parseChangedArgs(args: readonly string[]): ChangedOptions { } if (arg.startsWith("-")) { - throw new Error(`Unknown option: ${arg}`); + throw new ValidationError(`Unknown option: ${arg}`); } positionals.push(arg); } if (positionals.length > 0) { - throw new Error(`Unexpected arguments: ${positionals.join(" ")}`); + throw new ValidationError(`Unexpected arguments: ${positionals.join(" ")}`); } const options: ChangedOptions = {}; diff --git a/sources/commands/login/index.ts b/sources/commands/login/index.ts index 45f6e36..62b7e4d 100644 --- a/sources/commands/login/index.ts +++ b/sources/commands/login/index.ts @@ -1,4 +1,5 @@ import type { Command, CommandContext } from "@/commands/types"; +import { ValidationError } from "@/errors"; import type { Environment } from "@/environment"; import { createDeveloperClient, createProxyClient } from "@/client"; import { @@ -125,7 +126,7 @@ async function handleLogin( } if (!token) { - throw new Error("Missing token."); + throw new ValidationError("Missing token."); } token = token.trim(); @@ -189,7 +190,7 @@ export function parseLoginArgs(args: readonly string[]): LoginOptions { if (arg === "--token") { const value = args[i + 1]; if (value === undefined) { - throw new Error("--token requires a value"); + throw new ValidationError("--token requires a value"); } token = value; i += 1; @@ -204,7 +205,7 @@ export function parseLoginArgs(args: readonly string[]): LoginOptions { if (arg === "--proxy") { const value = args[i + 1]; if (value === undefined) { - throw new Error("--proxy requires a value"); + throw new ValidationError("--proxy requires a value"); } proxy = value; i += 1; @@ -222,14 +223,14 @@ export function parseLoginArgs(args: readonly string[]): LoginOptions { } if (arg.startsWith("-")) { - throw new Error(`Unknown option: ${arg}`); + throw new ValidationError(`Unknown option: ${arg}`); } positionals.push(arg); } if (positionals.length > 0) { - throw new Error(`Unexpected arguments: ${positionals.join(" ")}`); + throw new ValidationError(`Unexpected arguments: ${positionals.join(" ")}`); } if (token && tokenStdin) { @@ -335,7 +336,7 @@ export async function validateProxyConnection( async function readTokenFromStdin(): Promise { if (process.stdin.isTTY) { - throw new Error("--token-stdin requires input via stdin."); + throw new ValidationError("--token-stdin requires input via stdin."); } const chunks: string[] = []; diff --git a/sources/commands/mcp/index.ts b/sources/commands/mcp/index.ts index dcc3b0a..dba9c4b 100644 --- a/sources/commands/mcp/index.ts +++ b/sources/commands/mcp/index.ts @@ -1,4 +1,5 @@ import type { Command } from "@/commands/types"; +import { ValidationError } from "@/errors"; import { serveMcpHttp, type McpHttpOptions } from "@/mcp/httpServer"; import { serveMcp } from "@/mcp/server"; import { @@ -31,7 +32,7 @@ export const mcpCommand: Command = { run: async (args, context) => { const [subcommand, ...remaining] = args; if (!subcommand) { - throw new Error("Missing MCP subcommand."); + throw new ValidationError("Missing MCP subcommand."); } if (subcommand === "serve") { diff --git a/sources/commands/stream/index.ts b/sources/commands/stream/index.ts index 655ab9f..fcbb54b 100644 --- a/sources/commands/stream/index.ts +++ b/sources/commands/stream/index.ts @@ -1,6 +1,5 @@ import type { Command, CommandContext } from "@/commands/types"; import { requireClientToken } from "@/client/clientApi"; -import Handlebars from "handlebars"; const SUPPORTED_EVENT_TYPES = [ // Conversations @@ -291,16 +290,19 @@ type WebhookPayload = { type WebhookConfig = { endpoint: string; - template: Handlebars.TemplateDelegate; + bodyTemplate: string; }; +function renderTemplate(template: string, vars: Record): string { + return template.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? ""); +} + function buildWebhook(options: StreamOptions): WebhookConfig | null { if (!options.webhookEndpoint || !options.webhookBody) { return null; } - const template = Handlebars.compile(options.webhookBody, { noEscape: true }); - return { endpoint: options.webhookEndpoint, template }; + return { endpoint: options.webhookEndpoint, bodyTemplate: options.webhookBody }; } async function handleEvent( @@ -385,21 +387,19 @@ async function sendWebhook( webhook: WebhookConfig, payload: WebhookPayload ): Promise { - let body: string; - try { - body = webhook.template(payload); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - console.error(`Webhook template failed: ${message}`); - return; - } + const vars: Record = { + message: payload.message, + agentMessage: payload.agentMessage, + event: payload.event, + timestamp: payload.timestamp, + raw: payload.raw, + }; + const body = renderTemplate(webhook.bodyTemplate, vars); try { const response = await fetch(webhook.endpoint, { method: "POST", - headers: { - "Content-Type": "application/json", - }, + headers: { "Content-Type": "application/json" }, body, }); @@ -413,6 +413,7 @@ async function sendWebhook( } } + function formatEvent( eventType: string, data: Record, From 0b2f11b69e45e2dd3da6e1bd8721a956a7d5c64f Mon Sep 17 00:00:00 2001 From: Giovanni Laquidara Date: Thu, 11 Jun 2026 17:43:20 +0100 Subject: [PATCH 13/13] fix: regenerate bun.lock from public npm (removes CodeArtifact URLs) --- bun.lock | 89 +++++++++++++++++++++++--------------------------------- 1 file changed, 37 insertions(+), 52 deletions(-) diff --git a/bun.lock b/bun.lock index 4086232..baf0eb2 100644 --- a/bun.lock +++ b/bun.lock @@ -2,10 +2,9 @@ "lockfileVersion": 1, "workspaces": { "": { - "name": "bee-cli", + "name": "@beeai/cli", "dependencies": { "date-fns": "^4.1.0", - "handlebars": "^4.7.8", "qrcode": "^1.5.4", "tweetnacl": "^1.0.3", }, @@ -36,90 +35,76 @@ "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@1.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA=="], - "@types/node": ["@types/node@22.19.7", "https://amazon-149122183214.d.codeartifact.us-west-2.amazonaws.com/npm/shared/@types/node/-/node-22.19.7.tgz", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw=="], + "@types/node": ["@types/node@25.9.3", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg=="], - "@types/qrcode": ["@types/qrcode@1.5.6", "https://amazon-149122183214.d.codeartifact.us-west-2.amazonaws.com/npm/shared/@types/qrcode/-/qrcode-1.5.6.tgz", { "dependencies": { "@types/node": "*" } }, "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw=="], + "@types/qrcode": ["@types/qrcode@1.5.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw=="], - "ansi-regex": ["ansi-regex@5.0.1", "https://amazon-149122183214.d.codeartifact.us-west-2.amazonaws.com/npm/shared/ansi-regex/-/ansi-regex-5.0.1.tgz", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "ansi-styles": ["ansi-styles@4.3.0", "https://amazon-149122183214.d.codeartifact.us-west-2.amazonaws.com/npm/shared/ansi-styles/-/ansi-styles-4.3.0.tgz", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - "bun-types": ["bun-types@1.3.8", "https://amazon-149122183214.d.codeartifact.us-west-2.amazonaws.com/npm/shared/bun-types/-/bun-types-1.3.8.tgz", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="], + "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], - "camelcase": ["camelcase@5.3.1", "https://amazon-149122183214.d.codeartifact.us-west-2.amazonaws.com/npm/shared/camelcase/-/camelcase-5.3.1.tgz", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="], + "camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="], - "cliui": ["cliui@6.0.0", "https://amazon-149122183214.d.codeartifact.us-west-2.amazonaws.com/npm/shared/cliui/-/cliui-6.0.0.tgz", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ=="], + "cliui": ["cliui@6.0.0", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ=="], - "color-convert": ["color-convert@2.0.1", "https://amazon-149122183214.d.codeartifact.us-west-2.amazonaws.com/npm/shared/color-convert/-/color-convert-2.0.1.tgz", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], - "color-name": ["color-name@1.1.4", "https://amazon-149122183214.d.codeartifact.us-west-2.amazonaws.com/npm/shared/color-name/-/color-name-1.1.4.tgz", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], - "date-fns": ["date-fns@4.1.0", "https://amazon-149122183214.d.codeartifact.us-west-2.amazonaws.com/npm/shared/date-fns/-/date-fns-4.1.0.tgz", {}, "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="], + "date-fns": ["date-fns@4.4.0", "", {}, "sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w=="], - "decamelize": ["decamelize@1.2.0", "https://amazon-149122183214.d.codeartifact.us-west-2.amazonaws.com/npm/shared/decamelize/-/decamelize-1.2.0.tgz", {}, "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="], + "decamelize": ["decamelize@1.2.0", "", {}, "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="], - "dijkstrajs": ["dijkstrajs@1.0.3", "https://amazon-149122183214.d.codeartifact.us-west-2.amazonaws.com/npm/shared/dijkstrajs/-/dijkstrajs-1.0.3.tgz", {}, "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA=="], + "dijkstrajs": ["dijkstrajs@1.0.3", "", {}, "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA=="], - "emoji-regex": ["emoji-regex@8.0.0", "https://amazon-149122183214.d.codeartifact.us-west-2.amazonaws.com/npm/shared/emoji-regex/-/emoji-regex-8.0.0.tgz", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - "find-up": ["find-up@4.1.0", "https://amazon-149122183214.d.codeartifact.us-west-2.amazonaws.com/npm/shared/find-up/-/find-up-4.1.0.tgz", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], + "find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], - "get-caller-file": ["get-caller-file@2.0.5", "https://amazon-149122183214.d.codeartifact.us-west-2.amazonaws.com/npm/shared/get-caller-file/-/get-caller-file-2.0.5.tgz", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], - "handlebars": ["handlebars@4.7.8", "https://amazon-149122183214.d.codeartifact.us-west-2.amazonaws.com/npm/shared/handlebars/-/handlebars-4.7.8.tgz", { "dependencies": { "minimist": "^1.2.5", "neo-async": "^2.6.2", "source-map": "^0.6.1", "wordwrap": "^1.0.0" }, "optionalDependencies": { "uglify-js": "^3.1.4" }, "bin": { "handlebars": "bin/handlebars" } }, "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ=="], + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], - "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "https://amazon-149122183214.d.codeartifact.us-west-2.amazonaws.com/npm/shared/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + "locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], - "locate-path": ["locate-path@5.0.0", "https://amazon-149122183214.d.codeartifact.us-west-2.amazonaws.com/npm/shared/locate-path/-/locate-path-5.0.0.tgz", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], + "p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], - "minimist": ["minimist@1.2.8", "https://amazon-149122183214.d.codeartifact.us-west-2.amazonaws.com/npm/shared/minimist/-/minimist-1.2.8.tgz", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + "p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], - "neo-async": ["neo-async@2.6.2", "https://amazon-149122183214.d.codeartifact.us-west-2.amazonaws.com/npm/shared/neo-async/-/neo-async-2.6.2.tgz", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="], + "p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="], - "p-limit": ["p-limit@2.3.0", "https://amazon-149122183214.d.codeartifact.us-west-2.amazonaws.com/npm/shared/p-limit/-/p-limit-2.3.0.tgz", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], - "p-locate": ["p-locate@4.1.0", "https://amazon-149122183214.d.codeartifact.us-west-2.amazonaws.com/npm/shared/p-locate/-/p-locate-4.1.0.tgz", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], + "pngjs": ["pngjs@5.0.0", "", {}, "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw=="], - "p-try": ["p-try@2.2.0", "https://amazon-149122183214.d.codeartifact.us-west-2.amazonaws.com/npm/shared/p-try/-/p-try-2.2.0.tgz", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="], + "qrcode": ["qrcode@1.5.4", "", { "dependencies": { "dijkstrajs": "^1.0.1", "pngjs": "^5.0.0", "yargs": "^15.3.1" }, "bin": { "qrcode": "bin/qrcode" } }, "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg=="], - "path-exists": ["path-exists@4.0.0", "https://amazon-149122183214.d.codeartifact.us-west-2.amazonaws.com/npm/shared/path-exists/-/path-exists-4.0.0.tgz", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], - "pngjs": ["pngjs@5.0.0", "https://amazon-149122183214.d.codeartifact.us-west-2.amazonaws.com/npm/shared/pngjs/-/pngjs-5.0.0.tgz", {}, "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw=="], + "require-main-filename": ["require-main-filename@2.0.0", "", {}, "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="], - "qrcode": ["qrcode@1.5.4", "https://amazon-149122183214.d.codeartifact.us-west-2.amazonaws.com/npm/shared/qrcode/-/qrcode-1.5.4.tgz", { "dependencies": { "dijkstrajs": "^1.0.1", "pngjs": "^5.0.0", "yargs": "^15.3.1" }, "bin": { "qrcode": "bin/qrcode" } }, "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg=="], + "set-blocking": ["set-blocking@2.0.0", "", {}, "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="], - "require-directory": ["require-directory@2.1.1", "https://amazon-149122183214.d.codeartifact.us-west-2.amazonaws.com/npm/shared/require-directory/-/require-directory-2.1.1.tgz", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], + "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - "require-main-filename": ["require-main-filename@2.0.0", "https://amazon-149122183214.d.codeartifact.us-west-2.amazonaws.com/npm/shared/require-main-filename/-/require-main-filename-2.0.0.tgz", {}, "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="], + "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "set-blocking": ["set-blocking@2.0.0", "https://amazon-149122183214.d.codeartifact.us-west-2.amazonaws.com/npm/shared/set-blocking/-/set-blocking-2.0.0.tgz", {}, "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="], + "tweetnacl": ["tweetnacl@1.0.3", "", {}, "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw=="], - "source-map": ["source-map@0.6.1", "https://amazon-149122183214.d.codeartifact.us-west-2.amazonaws.com/npm/shared/source-map/-/source-map-0.6.1.tgz", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - "string-width": ["string-width@4.2.3", "https://amazon-149122183214.d.codeartifact.us-west-2.amazonaws.com/npm/shared/string-width/-/string-width-4.2.3.tgz", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + "undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], - "strip-ansi": ["strip-ansi@6.0.1", "https://amazon-149122183214.d.codeartifact.us-west-2.amazonaws.com/npm/shared/strip-ansi/-/strip-ansi-6.0.1.tgz", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "which-module": ["which-module@2.0.1", "", {}, "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ=="], - "tweetnacl": ["tweetnacl@1.0.3", "https://amazon-149122183214.d.codeartifact.us-west-2.amazonaws.com/npm/shared/tweetnacl/-/tweetnacl-1.0.3.tgz", {}, "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw=="], + "wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], - "typescript": ["typescript@5.9.3", "https://amazon-149122183214.d.codeartifact.us-west-2.amazonaws.com/npm/shared/typescript/-/typescript-5.9.3.tgz", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + "y18n": ["y18n@4.0.3", "", {}, "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="], - "uglify-js": ["uglify-js@3.19.3", "https://amazon-149122183214.d.codeartifact.us-west-2.amazonaws.com/npm/shared/uglify-js/-/uglify-js-3.19.3.tgz", { "bin": { "uglifyjs": "bin/uglifyjs" } }, "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ=="], + "yargs": ["yargs@15.4.1", "", { "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", "find-up": "^4.1.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^18.1.2" } }, "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A=="], - "undici-types": ["undici-types@6.21.0", "https://amazon-149122183214.d.codeartifact.us-west-2.amazonaws.com/npm/shared/undici-types/-/undici-types-6.21.0.tgz", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - - "which-module": ["which-module@2.0.1", "https://amazon-149122183214.d.codeartifact.us-west-2.amazonaws.com/npm/shared/which-module/-/which-module-2.0.1.tgz", {}, "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ=="], - - "wordwrap": ["wordwrap@1.0.0", "https://amazon-149122183214.d.codeartifact.us-west-2.amazonaws.com/npm/shared/wordwrap/-/wordwrap-1.0.0.tgz", {}, "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q=="], - - "wrap-ansi": ["wrap-ansi@6.2.0", "https://amazon-149122183214.d.codeartifact.us-west-2.amazonaws.com/npm/shared/wrap-ansi/-/wrap-ansi-6.2.0.tgz", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], - - "y18n": ["y18n@4.0.3", "https://amazon-149122183214.d.codeartifact.us-west-2.amazonaws.com/npm/shared/y18n/-/y18n-4.0.3.tgz", {}, "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="], - - "yargs": ["yargs@15.4.1", "https://amazon-149122183214.d.codeartifact.us-west-2.amazonaws.com/npm/shared/yargs/-/yargs-15.4.1.tgz", { "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", "find-up": "^4.1.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^18.1.2" } }, "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A=="], - - "yargs-parser": ["yargs-parser@18.1.3", "https://amazon-149122183214.d.codeartifact.us-west-2.amazonaws.com/npm/shared/yargs-parser/-/yargs-parser-18.1.3.tgz", { "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" } }, "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ=="], - - "bun-types/@types/node": ["@types/node@20.19.30", "https://amazon-149122183214.d.codeartifact.us-west-2.amazonaws.com/npm/shared/@types/node/-/node-20.19.30.tgz", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g=="], + "yargs-parser": ["yargs-parser@18.1.3", "", { "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" } }, "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ=="], } }