Allison/eng 1784 bundle daemon and cli with wui for single installation (#385)

* feat(daemon): add dynamic port allocation support

- Always create HTTP server regardless of port configuration
- Support port 0 for dynamic allocation
- Output actual port to stdout when using dynamic allocation
- Enable WUI to start daemon on any available port

* feat(wui): add daemon process management infrastructure

- Add Tauri shell and store plugins for process and state management
- Implement Rust daemon manager with automatic startup on app launch
- Add branch-based daemon isolation for development environments
- Create TypeScript services and hooks for daemon lifecycle management
- Add debug panel UI for manual daemon control in dev mode
- Support automatic daemon connection with managed port discovery
- Handle graceful daemon shutdown on app close
- Implement retry logic for daemon connection during startup

* feat(wui): add daemon process management infrastructure

* chore(wui): update gitignore for daemon artifacts

* build(wui): update Tauri bundle configuration

* build: add homebrew formula and update CI for daemon bundling

* formatting

* makefile to build daemon too (when run wui on dev)

* refactor: improve daemon management in WUI with async/await and better error handling

- Convert daemon start/stop operations to async/await pattern
- Switch HTTP host from 127.0.0.1 to localhost for better compatibility
- Add debug URL override support for development
- Improve error handling and startup behavior
- Fix path resolution for development mode when running from src-tauri

* daemon no longer randomly crashes
Needed to do weird stdout things

* Parse daemon log levels to improve dev logging output

Previously all daemon stderr output was logged as ERROR level, causing confusion
and double timestamps. Now we parse slog format to extract actual log levels
(INFO, WARN, ERROR, etc.) and route them to appropriate tracing levels.

* Integrate debug panel into status bar with theme-aware colors

- Move debug panel button from floating bottom-left to status bar bottom-right
- Replace Settings icon with Bug icon and match styling of other status controls
- Update connection status colors to use CSS variables for theme consistency
- Refactor DebugPanel to accept open/onOpenChange props for controlled state

* Remove local homebrew tap files - moved to separate repository

The homebrew tap is now properly maintained in its own repository
at humanlayer/homebrew-humanlayer for public distribution.

* Migrate WUI backend from tracing to log crate with tauri-plugin-log

- Replace tracing with log crate for consistency
- Add tauri-plugin-log with file rotation (50MB) and proper formatting
- Configure LogDir, Stdout (dev), and Webview targets
- Add proper prefixes: [Tauri] for WUI logs, [Daemon] for daemon logs
- Add log:default permission to capabilities

* Add frontend logging infrastructure for WUI

- Install @tauri-apps/plugin-log and @tauri-apps/api dependencies
- Create logging service with JSON serialization for objects
- Add log location notification for production builds
- Initialize attachConsole to display Rust logs in browser console
- All frontend logs will use [Console] prefix

* Migrate all console calls to use logger throughout WUI

- Replace 94 console.* calls with logger.* across 24 files
- Ensures all frontend logs are captured in production log file
- Maintains browser console output in development mode
- High-traffic files: NotificationService (22), AppStore (21), useSubscriptions (10)

* git ignore of compiled things

* fix it to not be annoyed

* Implement branch-based logging for WUI

- Extract branch detection logic (get_git_branch, extract_ticket_id, get_branch_id)
  into shared functions in lib.rs
- Configure tauri-plugin-log to use branch-based folders in development:
  ~/.humanlayer/logs/wui-{branch-id}/codelayer.log
- Update daemon.rs to use shared branch detection functions
- Add get_log_directory Tauri command to expose log paths to frontend
- Update log notification service to handle branch-based directories
- All log sources (WUI backend, daemon stderr, frontend console) now write
  to a single rotated log file per branch with clear source prefixes

* Update documentation and simplify Makefile for new logging structure

- Remove tee command from wui-dev target in Makefile since logging is now
  handled by tauri-plugin-log
- Update DEVELOPMENT.md to show new WUI log paths in environment overview
- Update CLAUDE.md files to document branch-based log locations:
  - Dev: ~/.humanlayer/logs/wui-{branch}/codelayer.log
  - Prod: Platform-specific directories
- Document that logs include all sources with prefixes: [Tauri], [Daemon], [Console]

* formatting

* add touch to bins

* ci: align GitHub Actions workflows with local development setup

- Simplify main.yml workflow to use unified setup process
- Add CI detection to setup_repo.sh for conditional tool installation
- Install CI-specific tools (Claude Code CLI, UV, golangci-lint) only in CI
- Add setup-ci and ci-tools targets to Makefile for explicit CI setup
- Parallelize npm and bun dependency installations for faster setup
- Add Go and Rust tool caching to improve CI performance
- Create platform-specific dependency script for future expansion

This change reduces workflow complexity from 75+ to ~50 lines per job while
maintaining all functionality. CI and local development now share the same
setup process, improving maintainability and consistency.

* sdk needs to build in order for wui to check
This commit is contained in:
Allison Durham
2025-07-31 12:10:26 -07:00
committed by GitHub
parent a204045376
commit 877ec27b0a
52 changed files with 3417 additions and 314 deletions

View File

@@ -24,8 +24,14 @@ jobs:
with:
node-version: 22
- name: Install Claude Code CLI
run: npm install -g @anthropic-ai/claude-code
- name: Install bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- uses: actions/setup-go@v5
with:
go-version: stable
- name: Install uv
uses: astral-sh/setup-uv@v4
@@ -38,34 +44,6 @@ jobs:
with:
python-version-file: "pyproject.toml"
- name: Install the project
run: uv sync --all-extras --dev
- name: Install bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install ts deps
run: npm -C humanlayer-ts install && npm -C hlyr install && npm -C humanlayer-ts-vercel-ai-sdk install
- name: Build hld SDK
run: cd hld/sdk/typescript && bun install && bun run build
- name: Install wui deps
run: bun install --cwd=humanlayer-wui
- uses: actions/setup-go@v5
with:
go-version: stable
# TODO: Update this, let's just pull the binary?
- name: Install golangci-lint
run: go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
- name: Install mockgen
run: go install go.uber.org/mock/mockgen@latest
- name: Install Rust
uses: dtolnay/rust-toolchain@1.83.0
with:
@@ -78,11 +56,29 @@ jobs:
packages: libwebkit2gtk-4.1-dev build-essential curl wget file libssl-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev
version: 1.0
- name: Build CLI
run: npm -C hlyr run build
- name: Cache Go tools
uses: actions/cache@v3
with:
path: ~/go/bin
key: go-tools-${{ runner.os }}-${{ hashFiles('**/go.mod') }}
restore-keys: |
go-tools-${{ runner.os }}-
- name: Generate Go mocks
run: make -C hld mocks
- name: Cache Rust tools
uses: actions/cache@v3
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: rust-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}
- name: Run repository setup
run: make setup-ci
env:
CI: true
- name: Run checks
run: make check
@@ -100,11 +96,15 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: 22
cache: "npm"
cache-dependency-path: humanlayer-ts/package-lock.json
- name: Install Claude Code CLI
run: npm install -g @anthropic-ai/claude-code
- name: Install bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- uses: actions/setup-go@v5
with:
go-version: stable
- name: Install uv
uses: astral-sh/setup-uv@v4
@@ -115,16 +115,6 @@ jobs:
- name: Set up Python ${{ matrix.python-version }}
run: uv python install ${{ matrix.python-version }}
- name: Install ts deps
run: npm -C humanlayer-ts install && npm -C hlyr install && npm -C humanlayer-ts-vercel-ai-sdk install
- uses: actions/setup-go@v5
with:
go-version: stable
- name: Install mockgen
run: go install go.uber.org/mock/mockgen@latest
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
@@ -135,22 +125,29 @@ jobs:
packages: libwebkit2gtk-4.1-dev build-essential curl wget file libssl-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev
version: 1.0
- name: Build CLI
run: npm -C hlyr run build
- name: Generate Go mocks
run: make -C hld mocks
- name: Install bun
uses: oven-sh/setup-bun@v2
- name: Cache Go tools
uses: actions/cache@v3
with:
bun-version: latest
path: ~/go/bin
key: go-tools-${{ runner.os }}-${{ hashFiles('**/go.mod') }}
restore-keys: |
go-tools-${{ runner.os }}-
- name: Build hld SDK
run: cd hld/sdk/typescript && bun install && bun run build
- name: Cache Rust tools
uses: actions/cache@v3
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: rust-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}
- name: Install wui deps
run: bun install --cwd=humanlayer-wui
- name: Run repository setup
run: make setup-ci
env:
CI: true
- name: Test
run: make test

View File

@@ -48,16 +48,32 @@ jobs:
working-directory: humanlayer-wui
run: bun install
- name: Build daemon for macOS ARM
working-directory: hld
run: GOOS=darwin GOARCH=arm64 go build -o hld-darwin-arm64 ./cmd/hld
- name: Build humanlayer CLI for macOS ARM
working-directory: hlyr
run: |
bun install
bun run build
bun build ./dist/index.js --compile --target=bun-darwin-arm64 --outfile=humanlayer-darwin-arm64
chmod +x humanlayer-darwin-arm64
- name: Copy binaries to Tauri resources
run: |
mkdir -p humanlayer-wui/src-tauri/bin
cp hld/hld-darwin-arm64 humanlayer-wui/src-tauri/bin/hld
cp hlyr/humanlayer-darwin-arm64 humanlayer-wui/src-tauri/bin/humanlayer
chmod +x humanlayer-wui/src-tauri/bin/hld
chmod +x humanlayer-wui/src-tauri/bin/humanlayer
- name: Build Tauri app (including DMG)
working-directory: humanlayer-wui
run: bun run tauri build
env:
APPLE_SIGNING_IDENTITY: "-" # Ad-hoc signing to prevent "damaged" error
- name: Build daemon for macOS ARM
working-directory: hld
run: GOOS=darwin GOARCH=arm64 go build -o hld-darwin-arm64 ./cmd/hld
- name: Upload DMG artifact
uses: actions/upload-artifact@v4
with:

View File

@@ -47,7 +47,8 @@ npx humanlayer launch "implement feature X" --daemon-socket ~/.humanlayer/daemon
| Daemon Binary | `hld/hld-nightly` | `hld/hld-dev` |
| Socket Path | `~/.humanlayer/daemon.sock` | `~/.humanlayer/daemon-dev.sock` |
| Database | `~/.humanlayer/daemon.db` | `~/.humanlayer/dev/daemon-TIMESTAMP.db` |
| Log Files | `daemon-nightly-*.log` | `daemon-dev-*.log` |
| Daemon Logs | `daemon-nightly-*.log` | `daemon-dev-*.log` |
| WUI Logs | Platform-specific | `~/.humanlayer/logs/wui-{branch}/codelayer.log` |
| WUI | Installed in `~/Applications` | Running in dev mode |
### Available Commands
@@ -177,6 +178,7 @@ HUMANLAYER_DATABASE_PATH=~/.humanlayer/dev/daemon-20240717-143022.db make daemon
**Q: How do I know which environment I'm in?**
- Check WUI title bar: shows "dev" for dev daemon
- Check daemon logs: `tail -f ~/.humanlayer/logs/daemon-*.log`
- Check WUI logs: `tail -f ~/.humanlayer/logs/wui-*/codelayer.log`
## Other Development Commands

View File

@@ -1,6 +1,19 @@
.PHONY: setup
setup: ## Set up the repository with all dependencies and builds
hack/setup_repo.sh
# CI-specific targets
.PHONY: setup-ci ci-tools
## CI Setup Commands
setup-ci: ci-tools setup ## Complete CI setup including CI-specific tools
@echo "✅ CI setup complete"
ci-tools: ## Install CI-specific tools
@echo "Installing CI-specific tools..."
@command -v claude >/dev/null 2>&1 || npm install -g @anthropic-ai/claude-code
@command -v uv >/dev/null 2>&1 || (curl -LsSf https://astral.sh/uv/install.sh | sh)
@command -v golangci-lint >/dev/null 2>&1 || go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
thoughts:
humanlayer thoughts init --directory humanlayer
@@ -500,6 +513,39 @@ wui-nightly-build:
cp -r humanlayer-wui/src-tauri/target/release/bundle/macos/CodeLayer.app ~/Applications/
@echo "Installed WUI nightly to ~/Applications/CodeLayer.app"
# Build humanlayer binary for bundling
.PHONY: humanlayer-build
humanlayer-build:
@echo "Building humanlayer CLI binary..."
cd hlyr && bun install && bun run build
@echo "humanlayer binary built at hlyr/dist/index.js"
# Build humanlayer standalone binary (requires bun)
.PHONY: humanlayer-binary-darwin-arm64
humanlayer-binary-darwin-arm64: humanlayer-build
@echo "Creating standalone humanlayer binary for macOS ARM64..."
cd hlyr && bun build ./dist/index.js --compile --target=bun-darwin-arm64 --outfile=humanlayer-darwin-arm64
chmod +x hlyr/humanlayer-darwin-arm64
@echo "Standalone binary created at hlyr/humanlayer-darwin-arm64"
# Build CodeLayer with bundled daemon and humanlayer
.PHONY: codelayer-bundle
codelayer-bundle:
@echo "Building daemon for bundling..."
cd hld && GOOS=darwin GOARCH=arm64 go build -o hld-darwin-arm64 ./cmd/hld
@echo "Building humanlayer for bundling..."
cd hlyr && bun install && bun run build
cd hlyr && bun build ./dist/index.js --compile --target=bun-darwin-arm64 --outfile=humanlayer-darwin-arm64
@echo "Copying binaries to CodeLayer resources..."
mkdir -p humanlayer-wui/src-tauri/bin
cp hld/hld-darwin-arm64 humanlayer-wui/src-tauri/bin/hld
cp hlyr/humanlayer-darwin-arm64 humanlayer-wui/src-tauri/bin/humanlayer
chmod +x humanlayer-wui/src-tauri/bin/hld
chmod +x humanlayer-wui/src-tauri/bin/humanlayer
@echo "Building CodeLayer with bundled binaries..."
cd humanlayer-wui && bun run tauri build
@echo "Build complete!"
# Open nightly WUI
.PHONY: wui-nightly
wui-nightly: wui-nightly-build
@@ -570,16 +616,18 @@ daemon-dev: daemon-dev-build
echo "$(TIMESTAMP) starting dev daemon in $$(pwd)" > ~/.humanlayer/logs/daemon-dev-$(TIMESTAMP).log
cd hld && HUMANLAYER_DATABASE_PATH=~/.humanlayer/daemon-dev.db \
HUMANLAYER_DAEMON_SOCKET=~/.humanlayer/daemon-dev.sock \
HUMANLAYER_DAEMON_HTTP_PORT=0 \
HUMANLAYER_DAEMON_VERSION_OVERRIDE=$$(git branch --show-current) \
./run-with-logging.sh ~/.humanlayer/logs/daemon-dev-$(TIMESTAMP).log ./hld-dev
# Run dev WUI with custom socket
.PHONY: wui-dev
wui-dev:
@mkdir -p ~/.humanlayer/logs
$(eval TIMESTAMP := $(shell date +%Y-%m-%d-%H-%M-%S))
echo "$(TIMESTAMP) starting dev wui in $$(pwd)" > ~/.humanlayer/logs/wui-dev-$(TIMESTAMP).log
cd humanlayer-wui && HUMANLAYER_DAEMON_SOCKET=~/.humanlayer/daemon-dev.sock bun run tauri dev 2>&1 | tee -a ~/.humanlayer/logs/wui-dev-$(TIMESTAMP).log
cd humanlayer-wui && HUMANLAYER_DAEMON_SOCKET=~/.humanlayer/daemon-dev.sock bun run tauri dev
# Alias for wui-dev that ensures daemon is built first
.PHONY: codelayer-dev
codelayer-dev: daemon-dev-build wui-dev
# Show current dev environment setup
.PHONY: dev-status

17
hack/install_platform_deps.sh Executable file
View File

@@ -0,0 +1,17 @@
#!/usr/bin/env bash
set -e
# Detect platform
case "$(uname -s)" in
Linux*)
if [ -n "$CI" ]; then
echo "📦 Installing Linux-specific dependencies for CI..."
# Note: apt packages are handled by GitHub Actions cache
# This is a placeholder for any additional Linux setup
fi
;;
Darwin*)
# macOS-specific setup if needed
;;
esac

View File

@@ -5,52 +5,84 @@
set -e # Exit on any error
# Helper function to run commands silently unless they fail
run_silent() {
local description="$1"
shift
local temp_output=$(mktemp)
# Source the run_silent utility
source hack/run_silent.sh
if "$@" > "$temp_output" 2>&1; then
rm "$temp_output"
else
cat "$temp_output"
echo
echo "$description failed. Please check the output above."
rm "$temp_output"
exit 1
# Detect if running in CI
if [ -n "$CI" ] || [ -n "$GITHUB_ACTIONS" ]; then
IS_CI=true
else
IS_CI=false
fi
# Function to install CI-specific tools
install_ci_tools() {
echo "🔧 Installing CI-specific tools..."
# Install Claude Code CLI
run_silent "Installing Claude Code CLI" "npm install -g @anthropic-ai/claude-code"
# Install UV for Python
if ! command -v uv &> /dev/null; then
run_silent "Installing UV" "curl -LsSf https://astral.sh/uv/install.sh | sh"
fi
# Install golangci-lint
if ! command -v golangci-lint &> /dev/null; then
run_silent "Installing golangci-lint" "go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest"
fi
}
echo "🔧 Setting up HumanLayer repository..."
# Main setup flow
echo "🚀 Setting up HumanLayer repository..."
# Install CI tools if in CI environment
if [ "$IS_CI" = true ]; then
install_ci_tools
fi
# Install mockgen if not already installed
if ! command -v mockgen &> /dev/null; then
echo "📦 Installing mockgen..."
run_silent "mockgen installation" go install go.uber.org/mock/mockgen@latest
run_silent "mockgen installation" "go install go.uber.org/mock/mockgen@latest"
else
echo "✓ mockgen already installed"
fi
# Repository-specific setup commands
echo "📦 Generating HLD mocks..."
run_silent "HLD mock generation" make -C hld mocks
run_silent "HLD mock generation" "make -C hld mocks"
echo "📦 Installing NPM dependencies..."
run_silent "hlyr npm install" npm i -C hlyr
run_silent "humanlayer-ts npm install" npm i -C humanlayer-ts
run_silent "humanlayer-ts-vercel-ai-sdk npm install" npm i -C humanlayer-ts-vercel-ai-sdk
# Install npm dependencies in parallel
(
run_silent "hlyr npm install" "npm i -C hlyr" &
run_silent "humanlayer-ts npm install" "npm i -C humanlayer-ts" &
run_silent "humanlayer-ts-vercel-ai-sdk npm install" "npm i -C humanlayer-ts-vercel-ai-sdk" &
wait
)
echo "📦 Installing HLD SDK dependencies..."
run_silent "hld-sdk bun install" bun install --cwd=hld/sdk/typescript
run_silent "hld-sdk bun install" "bun install --cwd=hld/sdk/typescript"
echo "🏗️ Building HLD TypeScript SDK..."
run_silent "hld-sdk build" sh -c "cd hld/sdk/typescript && bun run build"
run_silent "hld-sdk build" "sh -c 'cd hld/sdk/typescript && bun run build'"
echo "📦 Installing WUI dependencies..."
run_silent "humanlayer-wui bun install" bun install --cwd=humanlayer-wui
run_silent "humanlayer-wui bun install" "bun install --cwd=humanlayer-wui"
# Install Python dependencies if uv is available
if command -v uv &> /dev/null; then
echo "🐍 Setting up Python environment..."
run_silent "Installing Python dependencies" "uv sync --all-extras --dev"
fi
echo "🔧 Creating placeholder binaries for Tauri..."
mkdir -p humanlayer-wui/src-tauri/bin
touch humanlayer-wui/src-tauri/bin/hld
touch humanlayer-wui/src-tauri/bin/humanlayer
echo "🏗️ Building hlyr (requires mocks and npm dependencies)..."
run_silent "hlyr build" npm run build -C hlyr
run_silent "hlyr build" "npm run build -C hlyr"
echo "✅ Repository setup complete!"

3
hld/.gitignore vendored
View File

@@ -26,3 +26,6 @@ approval/mock_approval.go
client/mock_client.go
bus/mock_bus.go
store/mock_store.go
# git ignore the built binary for specific platforms.
hld-darwin-arm64

View File

@@ -2,7 +2,11 @@ This is the humanlayer Daemon (HLD) that powers the WUI (humanlayer-wui)
You cannot run this process, you cannot restart it. If you make changes, you must ask the user to rebuild it.
The logs are in ~/.humanlayer/logs/daemon-*.log
The daemon logs are in ~/.humanlayer/logs/daemon-*.log (timestamped files created by the Makefile when running with `make daemon-dev` or `make daemon-nightly`)
WUI logs (which include daemon stderr output) are in:
- Development: `~/.humanlayer/logs/wui-{branch}/codelayer.log`
- Production: Platform-specific log directories
It uses a database at ~/.humanlayer/*.db - you can access it with sqlite3 to inspect progress and debug things.

View File

@@ -115,12 +115,9 @@ func New() (*Daemon, error) {
approvalManager := approval.NewManager(conversationStore, eventBus)
slog.Debug("local approval manager created successfully")
// Create HTTP server if enabled
var httpServer *HTTPServer
if cfg.HTTPPort > 0 {
slog.Info("creating HTTP server", "port", cfg.HTTPPort)
httpServer = NewHTTPServer(cfg, sessionManager, approvalManager, conversationStore, eventBus)
}
// Create HTTP server (always enabled, port 0 means dynamic allocation)
slog.Info("creating HTTP server", "port", cfg.HTTPPort)
httpServer := NewHTTPServer(cfg, sessionManager, approvalManager, conversationStore, eventBus)
return &Daemon{
config: cfg,

View File

@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"log/slog"
"net"
"net/http"
"time"
@@ -90,17 +91,35 @@ func (s *HTTPServer) Start(ctx context.Context) error {
// Register SSE endpoint directly (not part of strict interface)
v1.GET("/stream/events", s.sseHandler.StreamEvents)
// Create HTTP server
// Create listener first to handle port 0
addr := fmt.Sprintf("%s:%d", s.config.HTTPHost, s.config.HTTPPort)
listener, err := net.Listen("tcp", addr)
if err != nil {
return fmt.Errorf("failed to listen on %s: %w", addr, err)
}
// Get actual port after binding
actualAddr := listener.Addr().(*net.TCPAddr)
actualPort := actualAddr.Port
// If port 0 was used, output actual port to stdout
if s.config.HTTPPort == 0 {
fmt.Printf("HTTP_PORT=%d\n", actualPort)
}
slog.Info("Starting HTTP server",
"configured_port", s.config.HTTPPort,
"actual_address", actualAddr.String())
// Create HTTP server
s.server = &http.Server{
Addr: addr,
Handler: s.router,
}
// Start server in goroutine
go func() {
slog.Info("Starting HTTP server", "address", addr)
if err := s.server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
// Use the existing listener
if err := s.server.Serve(listener); err != nil && err != http.ErrServerClosed {
slog.Error("HTTP server error", "error", err)
}
}()

3
hlyr/.gitignore vendored
View File

@@ -93,3 +93,6 @@ temp/
# test-mcp config files
mcp-config.json
# git ignore the build version for specific compiled platforms
humanlayer-darwin-arm64

771
hlyr/bun.lock Normal file
View File

@@ -0,0 +1,771 @@
{
"lockfileVersion": 1,
"workspaces": {
"": {
"name": "humanlayer",
"dependencies": {
"@humanlayer/sdk": "^0.7.7",
"@modelcontextprotocol/sdk": "^1.12.0",
"chalk": "^5.4.1",
"commander": "^14.0.0",
"dotenv": "^16.5.0",
"play-sound": "^1.1.6",
},
"devDependencies": {
"@types/node": "^22.15.21",
"@typescript-eslint/eslint-plugin": "^8.32.1",
"@typescript-eslint/parser": "^8.32.1",
"eslint": "^8.57.0",
"eslint-config-prettier": "^10.1.5",
"eslint-plugin-prettier": "^5.4.0",
"prettier": "^3.5.3",
"tsup": "^8.5.0",
"typescript": "^5.8.3",
"vitest": "^3.1.4",
},
},
},
"packages": {
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q=="],
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.4", "", { "os": "android", "cpu": "arm" }, "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ=="],
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.4", "", { "os": "android", "cpu": "arm64" }, "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A=="],
"@esbuild/android-x64": ["@esbuild/android-x64@0.25.4", "", { "os": "android", "cpu": "x64" }, "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ=="],
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g=="],
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A=="],
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ=="],
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ=="],
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.4", "", { "os": "linux", "cpu": "arm" }, "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ=="],
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ=="],
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.4", "", { "os": "linux", "cpu": "ia32" }, "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ=="],
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.4", "", { "os": "linux", "cpu": "none" }, "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA=="],
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.4", "", { "os": "linux", "cpu": "none" }, "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg=="],
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag=="],
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.4", "", { "os": "linux", "cpu": "none" }, "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA=="],
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g=="],
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.4", "", { "os": "linux", "cpu": "x64" }, "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA=="],
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.4", "", { "os": "none", "cpu": "arm64" }, "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ=="],
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.4", "", { "os": "none", "cpu": "x64" }, "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw=="],
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.4", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A=="],
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.4", "", { "os": "openbsd", "cpu": "x64" }, "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw=="],
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.4", "", { "os": "sunos", "cpu": "x64" }, "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q=="],
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ=="],
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg=="],
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.4", "", { "os": "win32", "cpu": "x64" }, "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ=="],
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.7.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw=="],
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="],
"@eslint/eslintrc": ["@eslint/eslintrc@2.1.4", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^9.6.0", "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ=="],
"@eslint/js": ["@eslint/js@8.57.1", "", {}, "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q=="],
"@humanlayer/sdk": ["@humanlayer/sdk@0.7.8", "", {}, "sha512-A2AJqpstF7ISxTj8w3UN9Z60WzukDmxmInR4gq5ZARepF0HHQfEsB5v5ZMVQn5TN+/l3+T2Dwf2TT5f9JChl8Q=="],
"@humanwhocodes/config-array": ["@humanwhocodes/config-array@0.13.0", "", { "dependencies": { "@humanwhocodes/object-schema": "^2.0.3", "debug": "^4.3.1", "minimatch": "^3.0.5" } }, "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw=="],
"@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="],
"@humanwhocodes/object-schema": ["@humanwhocodes/object-schema@2.0.3", "", {}, "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA=="],
"@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="],
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="],
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
"@jridgewell/set-array": ["@jridgewell/set-array@1.2.1", "", {}, "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A=="],
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.12.0", "", { "dependencies": { "ajv": "^6.12.6", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-m//7RlINx1F3sz3KqwY1WWzVgTcYX52HYk4bJ1hkBXV3zccAEth+jRvG8DBRrdaQuRsPAJOx2MH3zaHNCKL7Zg=="],
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
"@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
"@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
"@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
"@pkgr/core": ["@pkgr/core@0.2.4", "", {}, "sha512-ROFF39F6ZrnzSUEmQQZUar0Jt4xVoP9WnDRdWwF4NNcXs3xBTLgBUDoOwW141y1jP+S8nahIbdxbFC7IShw9Iw=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.41.0", "", { "os": "android", "cpu": "arm" }, "sha512-KxN+zCjOYHGwCl4UCtSfZ6jrq/qi88JDUtiEFk8LELEHq2Egfc/FgW+jItZiOLRuQfb/3xJSgFuNPC9jzggX+A=="],
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.41.0", "", { "os": "android", "cpu": "arm64" }, "sha512-yDvqx3lWlcugozax3DItKJI5j05B0d4Kvnjx+5mwiUpWramVvmAByYigMplaoAQ3pvdprGCTCE03eduqE/8mPQ=="],
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.41.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-2KOU574vD3gzcPSjxO0eyR5iWlnxxtmW1F5CkNOHmMlueKNCQkxR6+ekgWyVnz6zaZihpUNkGxjsYrkTJKhkaw=="],
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.41.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-gE5ACNSxHcEZyP2BA9TuTakfZvULEW4YAOtxl/A/YDbIir/wPKukde0BNPlnBiP88ecaN4BJI2TtAd+HKuZPQQ=="],
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.41.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-GSxU6r5HnWij7FoSo7cZg3l5GPg4HFLkzsFFh0N/b16q5buW1NAWuCJ+HMtIdUEi6XF0qH+hN0TEd78laRp7Dg=="],
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.41.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-KGiGKGDg8qLRyOWmk6IeiHJzsN/OYxO6nSbT0Vj4MwjS2XQy/5emsmtoqLAabqrohbgLWJ5GV3s/ljdrIr8Qjg=="],
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.41.0", "", { "os": "linux", "cpu": "arm" }, "sha512-46OzWeqEVQyX3N2/QdiU/CMXYDH/lSHpgfBkuhl3igpZiaB3ZIfSjKuOnybFVBQzjsLwkus2mjaESy8H41SzvA=="],
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.41.0", "", { "os": "linux", "cpu": "arm" }, "sha512-lfgW3KtQP4YauqdPpcUZHPcqQXmTmH4nYU0cplNeW583CMkAGjtImw4PKli09NFi2iQgChk4e9erkwlfYem6Lg=="],
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.41.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-nn8mEyzMbdEJzT7cwxgObuwviMx6kPRxzYiOl6o/o+ChQq23gfdlZcUNnt89lPhhz3BYsZ72rp0rxNqBSfqlqw=="],
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.41.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-l+QK99je2zUKGd31Gh+45c4pGDAqZSuWQiuRFCdHYC2CSiO47qUWsCcenrI6p22hvHZrDje9QjwSMAFL3iwXwQ=="],
"@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.41.0", "", { "os": "linux", "cpu": "none" }, "sha512-WbnJaxPv1gPIm6S8O/Wg+wfE/OzGSXlBMbOe4ie+zMyykMOeqmgD1BhPxZQuDqwUN+0T/xOFtL2RUWBspnZj3w=="],
"@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.41.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eRDWR5t67/b2g8Q/S8XPi0YdbKcCs4WQ8vklNnUYLaSWF+Cbv2axZsp4jni6/j7eKvMLYCYdcsv8dcU+a6QNFg=="],
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.41.0", "", { "os": "linux", "cpu": "none" }, "sha512-TWrZb6GF5jsEKG7T1IHwlLMDRy2f3DPqYldmIhnA2DVqvvhY2Ai184vZGgahRrg8k9UBWoSlHv+suRfTN7Ua4A=="],
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.41.0", "", { "os": "linux", "cpu": "none" }, "sha512-ieQljaZKuJpmWvd8gW87ZmSFwid6AxMDk5bhONJ57U8zT77zpZ/TPKkU9HpnnFrM4zsgr4kiGuzbIbZTGi7u9A=="],
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.41.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-/L3pW48SxrWAlVsKCN0dGLB2bi8Nv8pr5S5ocSM+S0XCn5RCVCXqi8GVtHFsOBBCSeR+u9brV2zno5+mg3S4Aw=="],
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.41.0", "", { "os": "linux", "cpu": "x64" }, "sha512-XMLeKjyH8NsEDCRptf6LO8lJk23o9wvB+dJwcXMaH6ZQbbkHu2dbGIUindbMtRN6ux1xKi16iXWu6q9mu7gDhQ=="],
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.41.0", "", { "os": "linux", "cpu": "x64" }, "sha512-m/P7LycHZTvSQeXhFmgmdqEiTqSV80zn6xHaQ1JSqwCtD1YGtwEK515Qmy9DcB2HK4dOUVypQxvhVSy06cJPEg=="],
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.41.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-4yodtcOrFHpbomJGVEqZ8fzD4kfBeCbpsUy5Pqk4RluXOdsWdjLnjhiKy2w3qzcASWd04fp52Xz7JKarVJ5BTg=="],
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.41.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-tmazCrAsKzdkXssEc65zIE1oC6xPHwfy9d5Ta25SRCDOZS+I6RypVVShWALNuU9bxIfGA0aqrmzlzoM5wO5SPQ=="],
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.41.0", "", { "os": "win32", "cpu": "x64" }, "sha512-h1J+Yzjo/X+0EAvR2kIXJDuTuyT7drc+t2ALY0nIcGPbTatNOf0VWdhEA2Z4AAjv6X1NJV7SYo5oCTYRJhSlVA=="],
"@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="],
"@types/node": ["@types/node@22.15.21", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ=="],
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.32.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.32.1", "@typescript-eslint/type-utils": "8.32.1", "@typescript-eslint/utils": "8.32.1", "@typescript-eslint/visitor-keys": "8.32.1", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-6u6Plg9nP/J1GRpe/vcjjabo6Uc5YQPAMxsgQyGC/I0RuukiG1wIe3+Vtg3IrSCVJDmqK3j8adrtzXSENRtFgg=="],
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.32.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.32.1", "@typescript-eslint/types": "8.32.1", "@typescript-eslint/typescript-estree": "8.32.1", "@typescript-eslint/visitor-keys": "8.32.1", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-LKMrmwCPoLhM45Z00O1ulb6jwyVr2kr3XJp+G+tSEZcbauNnScewcQwtJqXDhXeYPDEjZ8C1SjXm015CirEmGg=="],
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.32.1", "", { "dependencies": { "@typescript-eslint/types": "8.32.1", "@typescript-eslint/visitor-keys": "8.32.1" } }, "sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA=="],
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.32.1", "", { "dependencies": { "@typescript-eslint/typescript-estree": "8.32.1", "@typescript-eslint/utils": "8.32.1", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-mv9YpQGA8iIsl5KyUPi+FGLm7+bA4fgXaeRcFKRDRwDMu4iwrSHeDPipwueNXhdIIZltwCJv+NkxftECbIZWfA=="],
"@typescript-eslint/types": ["@typescript-eslint/types@8.32.1", "", {}, "sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg=="],
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.32.1", "", { "dependencies": { "@typescript-eslint/types": "8.32.1", "@typescript-eslint/visitor-keys": "8.32.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg=="],
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.32.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.32.1", "@typescript-eslint/types": "8.32.1", "@typescript-eslint/typescript-estree": "8.32.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA=="],
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.32.1", "", { "dependencies": { "@typescript-eslint/types": "8.32.1", "eslint-visitor-keys": "^4.2.0" } }, "sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w=="],
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
"@vitest/expect": ["@vitest/expect@3.1.4", "", { "dependencies": { "@vitest/spy": "3.1.4", "@vitest/utils": "3.1.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-xkD/ljeliyaClDYqHPNCiJ0plY5YIcM0OlRiZizLhlPmpXWpxnGMyTZXOHFhFeG7w9P5PBeL4IdtJ/HeQwTbQA=="],
"@vitest/mocker": ["@vitest/mocker@3.1.4", "", { "dependencies": { "@vitest/spy": "3.1.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^5.0.0 || ^6.0.0" }, "optionalPeers": ["msw"] }, "sha512-8IJ3CvwtSw/EFXqWFL8aCMu+YyYXG2WUSrQbViOZkWTKTVicVwZ/YiEZDSqD00kX+v/+W+OnxhNWoeVKorHygA=="],
"@vitest/pretty-format": ["@vitest/pretty-format@3.1.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-cqv9H9GvAEoTaoq+cYqUTCGscUjKqlJZC7PRwY5FMySVj5J+xOm1KQcCiYHJOEzOKRUhLH4R2pTwvFlWCEScsg=="],
"@vitest/runner": ["@vitest/runner@3.1.4", "", { "dependencies": { "@vitest/utils": "3.1.4", "pathe": "^2.0.3" } }, "sha512-djTeF1/vt985I/wpKVFBMWUlk/I7mb5hmD5oP8K9ACRmVXgKTae3TUOtXAEBfslNKPzUQvnKhNd34nnRSYgLNQ=="],
"@vitest/snapshot": ["@vitest/snapshot@3.1.4", "", { "dependencies": { "@vitest/pretty-format": "3.1.4", "magic-string": "^0.30.17", "pathe": "^2.0.3" } }, "sha512-JPHf68DvuO7vilmvwdPr9TS0SuuIzHvxeaCkxYcCD4jTk67XwL45ZhEHFKIuCm8CYstgI6LZ4XbwD6ANrwMpFg=="],
"@vitest/spy": ["@vitest/spy@3.1.4", "", { "dependencies": { "tinyspy": "^3.0.2" } }, "sha512-Xg1bXhu+vtPXIodYN369M86K8shGLouNjoVI78g8iAq2rFoHFdajNvJJ5A/9bPMFcfQqdaCpOgWKEoMQg/s0Yg=="],
"@vitest/utils": ["@vitest/utils@3.1.4", "", { "dependencies": { "@vitest/pretty-format": "3.1.4", "loupe": "^3.1.3", "tinyrainbow": "^2.0.0" } }, "sha512-yriMuO1cfFhmiGc8ataN51+9ooHRuURdfAZfwFd3usWynjzpLslZdYnRegTv32qdgtJTsj15FoeZe2g15fY1gg=="],
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
"acorn": ["acorn@8.14.1", "", { "bin": "bin/acorn" }, "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg=="],
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
"ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="],
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
"assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="],
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"body-parser": ["body-parser@2.2.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", "iconv-lite": "^0.6.3", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", "type-is": "^2.0.0" } }, "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg=="],
"brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="],
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
"bundle-require": ["bundle-require@5.1.0", "", { "dependencies": { "load-tsconfig": "^0.2.3" }, "peerDependencies": { "esbuild": ">=0.18" } }, "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA=="],
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
"cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="],
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
"chai": ["chai@5.2.0", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw=="],
"chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="],
"check-error": ["check-error@2.1.1", "", {}, "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw=="],
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
"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", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
"commander": ["commander@14.0.0", "", {}, "sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA=="],
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
"confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="],
"consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="],
"content-disposition": ["content-disposition@1.0.0", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg=="],
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
"cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
"cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
"cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="],
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
"debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
"deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="],
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
"doctrine": ["doctrine@3.0.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w=="],
"dotenv": ["dotenv@16.5.0", "", {}, "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg=="],
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
"eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
"emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
"es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="],
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
"esbuild": ["esbuild@0.25.4", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.4", "@esbuild/android-arm": "0.25.4", "@esbuild/android-arm64": "0.25.4", "@esbuild/android-x64": "0.25.4", "@esbuild/darwin-arm64": "0.25.4", "@esbuild/darwin-x64": "0.25.4", "@esbuild/freebsd-arm64": "0.25.4", "@esbuild/freebsd-x64": "0.25.4", "@esbuild/linux-arm": "0.25.4", "@esbuild/linux-arm64": "0.25.4", "@esbuild/linux-ia32": "0.25.4", "@esbuild/linux-loong64": "0.25.4", "@esbuild/linux-mips64el": "0.25.4", "@esbuild/linux-ppc64": "0.25.4", "@esbuild/linux-riscv64": "0.25.4", "@esbuild/linux-s390x": "0.25.4", "@esbuild/linux-x64": "0.25.4", "@esbuild/netbsd-arm64": "0.25.4", "@esbuild/netbsd-x64": "0.25.4", "@esbuild/openbsd-arm64": "0.25.4", "@esbuild/openbsd-x64": "0.25.4", "@esbuild/sunos-x64": "0.25.4", "@esbuild/win32-arm64": "0.25.4", "@esbuild/win32-ia32": "0.25.4", "@esbuild/win32-x64": "0.25.4" }, "bin": "bin/esbuild" }, "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q=="],
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
"eslint": ["eslint@8.57.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", "@eslint/js": "8.57.1", "@humanwhocodes/config-array": "^0.13.0", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", "eslint-scope": "^7.2.2", "eslint-visitor-keys": "^3.4.3", "espree": "^9.6.1", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "globals": "^13.19.0", "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3", "strip-ansi": "^6.0.1", "text-table": "^0.2.0" }, "bin": "bin/eslint.js" }, "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA=="],
"eslint-config-prettier": ["eslint-config-prettier@10.1.5", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": "bin/cli.js" }, "sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw=="],
"eslint-plugin-prettier": ["eslint-plugin-prettier@5.4.0", "", { "dependencies": { "prettier-linter-helpers": "^1.0.0", "synckit": "^0.11.0" }, "peerDependencies": { "@types/eslint": ">=8.0.0", "eslint": ">=8.0.0", "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", "prettier": ">=3.0.0" }, "optionalPeers": ["@types/eslint"] }, "sha512-BvQOvUhkVQM1i63iMETK9Hjud9QhqBnbtT1Zc642p9ynzBuCe5pybkOnvqZIBypXmMlsGcnU4HZ8sCTPfpAexA=="],
"eslint-scope": ["eslint-scope@7.2.2", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg=="],
"eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
"espree": ["espree@9.6.1", "", { "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.4.1" } }, "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ=="],
"esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="],
"esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
"estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
"estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
"eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="],
"eventsource-parser": ["eventsource-parser@3.0.2", "", {}, "sha512-6RxOBZ/cYgd8usLwsEl+EC09Au/9BcmCKYF2/xbml6DNczf7nv0MQb+7BA2F+li6//I+28VNlQR37XfQtcAJuA=="],
"expect-type": ["expect-type@1.2.1", "", {}, "sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw=="],
"express": ["express@5.1.0", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA=="],
"express-rate-limit": ["express-rate-limit@7.5.0", "", { "peerDependencies": { "express": "^4.11 || 5 || ^5.0.0-beta.1" } }, "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg=="],
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fast-diff": ["fast-diff@1.3.0", "", {}, "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw=="],
"fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
"fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
"fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="],
"fdir": ["fdir@6.4.4", "", { "peerDependencies": { "picomatch": "^3 || ^4" } }, "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg=="],
"file-entry-cache": ["file-entry-cache@6.0.1", "", { "dependencies": { "flat-cache": "^3.0.4" } }, "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg=="],
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
"finalhandler": ["finalhandler@2.1.0", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q=="],
"find-exec": ["find-exec@1.0.3", "", { "dependencies": { "shell-quote": "^1.8.1" } }, "sha512-gnG38zW90mS8hm5smNcrBnakPEt+cGJoiMkJwCU0IYnEb0H2NQk0NIljhNW+48oniCriFek/PH6QXbwsJo/qug=="],
"find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
"fix-dts-default-cjs-exports": ["fix-dts-default-cjs-exports@1.0.1", "", { "dependencies": { "magic-string": "^0.30.17", "mlly": "^1.7.4", "rollup": "^4.34.8" } }, "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg=="],
"flat-cache": ["flat-cache@3.2.0", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.3", "rimraf": "^3.0.2" } }, "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw=="],
"flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="],
"foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
"fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
"glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": "dist/esm/bin.mjs" }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="],
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
"globals": ["globals@13.24.0", "", { "dependencies": { "type-fest": "^0.20.2" } }, "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ=="],
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
"graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="],
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
"http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="],
"iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
"ignore": ["ignore@7.0.4", "", {}, "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A=="],
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
"inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="],
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
"is-path-inside": ["is-path-inside@3.0.3", "", {}, "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ=="],
"is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
"jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
"joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="],
"js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": "bin/js-yaml.js" }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="],
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
"json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
"lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="],
"lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
"load-tsconfig": ["load-tsconfig@0.2.5", "", {}, "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg=="],
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
"lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
"lodash.sortby": ["lodash.sortby@4.7.0", "", {}, "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA=="],
"loupe": ["loupe@3.1.3", "", {}, "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug=="],
"lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
"magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
"media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
"merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
"mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
"mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="],
"minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
"minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
"mlly": ["mlly@1.7.4", "", { "dependencies": { "acorn": "^8.14.0", "pathe": "^2.0.1", "pkg-types": "^1.3.0", "ufo": "^1.5.4" } }, "sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="],
"nanoid": ["nanoid@3.3.11", "", { "bin": "bin/nanoid.cjs" }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
"p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
"p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
"package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="],
"parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
"path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="],
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
"path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
"path-to-regexp": ["path-to-regexp@8.2.0", "", {}, "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ=="],
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
"pathval": ["pathval@2.0.0", "", {}, "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],
"pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="],
"pkce-challenge": ["pkce-challenge@5.0.0", "", {}, "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ=="],
"pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
"play-sound": ["play-sound@1.1.6", "", { "dependencies": { "find-exec": "1.0.3" } }, "sha512-09eO4QiXNFXJffJaOW5P6x6F5RLihpLUkXttvUZeWml0fU6x6Zp7AjG9zaeMpgH2ZNvq4GR1ytB22ddYcqJIZA=="],
"postcss": ["postcss@8.5.3", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A=="],
"postcss-load-config": ["postcss-load-config@6.0.1", "", { "dependencies": { "lilconfig": "^3.1.1" }, "peerDependencies": { "jiti": ">=1.21.0", "postcss": ">=8.0.9", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["jiti", "tsx", "yaml"] }, "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g=="],
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
"prettier": ["prettier@3.5.3", "", { "bin": "bin/prettier.cjs" }, "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw=="],
"prettier-linter-helpers": ["prettier-linter-helpers@1.0.0", "", { "dependencies": { "fast-diff": "^1.1.2" } }, "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w=="],
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
"qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="],
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
"raw-body": ["raw-body@3.0.0", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.6.3", "unpipe": "1.0.0" } }, "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g=="],
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
"resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="],
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
"rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": "bin.js" }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="],
"rollup": ["rollup@4.41.0", "", { "dependencies": { "@types/estree": "1.0.7" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.41.0", "@rollup/rollup-android-arm64": "4.41.0", "@rollup/rollup-darwin-arm64": "4.41.0", "@rollup/rollup-darwin-x64": "4.41.0", "@rollup/rollup-freebsd-arm64": "4.41.0", "@rollup/rollup-freebsd-x64": "4.41.0", "@rollup/rollup-linux-arm-gnueabihf": "4.41.0", "@rollup/rollup-linux-arm-musleabihf": "4.41.0", "@rollup/rollup-linux-arm64-gnu": "4.41.0", "@rollup/rollup-linux-arm64-musl": "4.41.0", "@rollup/rollup-linux-loongarch64-gnu": "4.41.0", "@rollup/rollup-linux-powerpc64le-gnu": "4.41.0", "@rollup/rollup-linux-riscv64-gnu": "4.41.0", "@rollup/rollup-linux-riscv64-musl": "4.41.0", "@rollup/rollup-linux-s390x-gnu": "4.41.0", "@rollup/rollup-linux-x64-gnu": "4.41.0", "@rollup/rollup-linux-x64-musl": "4.41.0", "@rollup/rollup-win32-arm64-msvc": "4.41.0", "@rollup/rollup-win32-ia32-msvc": "4.41.0", "@rollup/rollup-win32-x64-msvc": "4.41.0", "fsevents": "~2.3.2" }, "bin": "dist/bin/rollup" }, "sha512-HqMFpUbWlf/tvcxBFNKnJyzc7Lk+XO3FGc3pbNBLqEbOz0gPLRgcrlS3UF4MfUrVlstOaP/q0kM6GVvi+LrLRg=="],
"router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
"semver": ["semver@7.7.2", "", { "bin": "bin/semver.js" }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
"send": ["send@1.2.0", "", { "dependencies": { "debug": "^4.3.5", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.0", "mime-types": "^3.0.1", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.1" } }, "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw=="],
"serve-static": ["serve-static@2.2.0", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ=="],
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
"shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="],
"side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
"side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
"side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
"siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="],
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
"source-map": ["source-map@0.8.0-beta.0", "", { "dependencies": { "whatwg-url": "^7.0.0" } }, "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA=="],
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
"statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="],
"std-env": ["std-env@3.9.0", "", {}, "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw=="],
"string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
"string-width-cjs": ["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=="],
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
"sucrase": ["sucrase@3.35.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "glob": "^10.3.10", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA=="],
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"synckit": ["synckit@0.11.6", "", { "dependencies": { "@pkgr/core": "^0.2.4" } }, "sha512-2pR2ubZSV64f/vqm9eLPz/KOvR9Dm+Co/5ChLgeHl0yEDRc6h5hXHoxEQH8Y5Ljycozd3p1k5TTSVdzYGkPvLw=="],
"text-table": ["text-table@0.2.0", "", {}, "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw=="],
"thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="],
"thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="],
"tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="],
"tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="],
"tinyglobby": ["tinyglobby@0.2.13", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw=="],
"tinypool": ["tinypool@1.0.2", "", {}, "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA=="],
"tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="],
"tinyspy": ["tinyspy@3.0.2", "", {}, "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q=="],
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
"tr46": ["tr46@1.0.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA=="],
"tree-kill": ["tree-kill@1.2.2", "", { "bin": "cli.js" }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="],
"ts-api-utils": ["ts-api-utils@2.1.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ=="],
"ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="],
"tsup": ["tsup@8.5.0", "", { "dependencies": { "bundle-require": "^5.1.0", "cac": "^6.7.14", "chokidar": "^4.0.3", "consola": "^3.4.0", "debug": "^4.4.0", "esbuild": "^0.25.0", "fix-dts-default-cjs-exports": "^1.0.0", "joycon": "^3.1.1", "picocolors": "^1.1.1", "postcss-load-config": "^6.0.1", "resolve-from": "^5.0.0", "rollup": "^4.34.8", "source-map": "0.8.0-beta.0", "sucrase": "^3.35.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.11", "tree-kill": "^1.2.2" }, "peerDependencies": { "@microsoft/api-extractor": "^7.36.0", "@swc/core": "^1", "postcss": "^8.4.12", "typescript": ">=4.5.0" }, "optionalPeers": ["@microsoft/api-extractor", "@swc/core"], "bin": { "tsup": "dist/cli-default.js", "tsup-node": "dist/cli-node.js" } }, "sha512-VmBp77lWNQq6PfuMqCHD3xWl22vEoWsKajkF8t+yMBawlUS8JzEI+vOVMeuNZIuMML8qXRizFKi9oD5glKQVcQ=="],
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
"type-fest": ["type-fest@0.20.2", "", {}, "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ=="],
"type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="],
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
"ufo": ["ufo@1.6.1", "", {}, "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA=="],
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
"vite": ["vite@6.3.5", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": "bin/vite.js" }, "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ=="],
"vite-node": ["vite-node@3.1.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.0", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0" }, "bin": "vite-node.mjs" }, "sha512-6enNwYnpyDo4hEgytbmc6mYWHXDHYEn0D1/rw4Q+tnHUGtKTJsn8T1YkX6Q18wI5LCrS8CTYlBaiCqxOy2kvUA=="],
"vitest": ["vitest@3.1.4", "", { "dependencies": { "@vitest/expect": "3.1.4", "@vitest/mocker": "3.1.4", "@vitest/pretty-format": "^3.1.4", "@vitest/runner": "3.1.4", "@vitest/snapshot": "3.1.4", "@vitest/spy": "3.1.4", "@vitest/utils": "3.1.4", "chai": "^5.2.0", "debug": "^4.4.0", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.13", "tinypool": "^1.0.2", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0", "vite-node": "3.1.4", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.1.4", "@vitest/ui": "3.1.4", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": "vitest.mjs" }, "sha512-Ta56rT7uWxCSJXlBtKgIlApJnT6e6IGmTYxYcmxjJ4ujuZDI59GUQgVDObXXJujOmPDBYXHK1qmaGtneu6TNIQ=="],
"webidl-conversions": ["webidl-conversions@4.0.2", "", {}, "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg=="],
"whatwg-url": ["whatwg-url@7.1.0", "", { "dependencies": { "lodash.sortby": "^4.7.0", "tr46": "^1.0.1", "webidl-conversions": "^4.0.2" } }, "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg=="],
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": "cli.js" }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="],
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
"wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],
"wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
"zod": ["zod@3.25.30", "", {}, "sha512-VolhdEtu6TJr/fzGuHA/SZ5ixvXqA6ADOG9VRcQ3rdOKmF5hkmcJbyaQjUH5BgmpA9gej++zYRX7zjSmdReIwA=="],
"zod-to-json-schema": ["zod-to-json-schema@3.24.5", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g=="],
"@eslint/eslintrc/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
"@eslint/eslintrc/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
"@humanwhocodes/config-array/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
"@isaacs/cliui/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
"@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@4.2.0", "", {}, "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw=="],
"eslint/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"eslint/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
"import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
"string-width/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
"string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="],
"vite/fdir": ["fdir@6.4.4", "", { "peerDependencies": { "picomatch": "^3 || ^4" } }, "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg=="],
"vite/picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],
"wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="],
"wrap-ansi/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
"wrap-ansi-cjs/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"wrap-ansi-cjs/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=="],
"@eslint/eslintrc/minimatch/brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="],
"@humanwhocodes/config-array/minimatch/brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="],
"@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
"glob/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
"rimraf/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
"string-width/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
"wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
"rimraf/glob/minimatch/brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="],
}
}

View File

@@ -25,3 +25,7 @@ dist-ssr
# Generated Tauri config for local development
src-tauri/tauri.conf.local.json
# Bundled binaries (created during build)
src-tauri/bin/
!src-tauri/bin/.gitkeep

View File

@@ -2,7 +2,15 @@ This is the HumanLayer Web UI (WUI) - a desktop application for managing AI agen
The WUI connects to the HumanLayer daemon (hld) to provide a graphical interface for monitoring Claude Code sessions and responding to approval requests. It's built with Tauri for desktop packaging and React for the interface.
When the WUI is running, logs are written to ~/.humanlayer/logs/wui-TIMESTAMP.log for debugging purposes. The application hot-reloads automatically when you make changes to the code - you cannot manually restart it.
When the WUI is running, logs are written to:
- Development: `~/.humanlayer/logs/wui-{branch-id}/codelayer.log` (e.g., `wui-eng-1784/codelayer.log`)
- Production: Platform-specific directories:
- macOS: `~/Library/Logs/dev.humanlayer.wui/`
- Windows: `%APPDATA%\dev.humanlayer.wui\logs\`
- Linux: `~/.config/dev.humanlayer.wui/logs/`
Logs include output from the WUI backend, daemon stderr (prefixed with [Daemon]), and frontend console logs (prefixed with [Console]). The log files automatically rotate at 50MB. The application hot-reloads automatically when you make changes to the code - you cannot manually restart it.
The WUI communicates with the daemon via JSON-RPC over a Unix socket at ~/.humanlayer/daemon.sock. All session and approval data comes from the daemon - the WUI is purely a presentation layer.

View File

@@ -4,17 +4,106 @@ Web/desktop UI for the HumanLayer daemon (`hld`) built with Tauri and React.
## Development
### Running in Development Mode
1. Build the daemon (required for auto-launch):
```bash
make daemon-dev-build
```
2. Start CodeLayer in development mode:
```bash
make codelayer-dev
```
The daemon starts automatically and invisibly when the app launches. No manual daemon management needed.
### Disabling Auto-Launch (Advanced Users)
If you prefer to manage the daemon manually:
```bash
# Install dependencies
bun install
# Start dev server
bun run tauri dev
# Build for production
bun run build
export HUMANLAYER_WUI_AUTOLAUNCH_DAEMON=false
make codelayer-dev
```
### Using an External Daemon
To connect to a daemon running on a specific port:
```bash
export HUMANLAYER_DAEMON_HTTP_PORT=7777
make codelayer-dev
```
### Building for Production
To build CodeLayer with bundled daemon:
```bash
make codelayer-bundle
```
This will:
1. Build the daemon for macOS ARM64
2. Build the humanlayer CLI for macOS ARM64
3. Copy both to the Tauri resources
4. Build CodeLayer with the bundled binaries
The resulting DMG will include both binaries and automatically manage their lifecycle.
### Daemon Management
The daemon lifecycle is completely automatic:
**In development mode:**
- Daemon starts invisibly when CodeLayer launches
- Each git branch gets its own daemon instance
- Database is copied from `daemon-dev.db` to `daemon-{branch}.db`
- Socket and port are isolated per branch
- Use debug panel (bottom-left settings icon) for manual control if needed
**In production mode:**
- Daemon starts invisibly when CodeLayer launches
- Uses default paths (`~/.humanlayer/daemon.db`)
- Stops automatically when the app exits
- No user interaction or awareness required
**Error Handling:**
- If daemon fails to start, app continues normally
- Connection can be established later via debug panel (dev) or automatically on retry
- All errors are logged but never interrupt the user experience
### MCP Testing
To test MCP functionality:
**In development:**
- Ensure you have `humanlayer` installed globally: `npm install -g humanlayer`
- Start CodeLayer: `make codelayer-dev`
- Configure Claude Code to use `humanlayer mcp claude_approvals`
- The MCP server will connect to your running daemon
**In production (after Homebrew installation):**
- Claude Code can directly execute `humanlayer mcp claude_approvals`
- No npm or npx required - Homebrew automatically created symlinks in PATH
- The MCP server connects to the daemon started by CodeLayer
- Verify PATH setup is working: `which humanlayer` should show `/usr/local/bin/humanlayer`
**Troubleshooting MCP connection:**
- If MCP can't find `humanlayer`, restart Claude Code after installation
- If launched from Dock, Claude Code may have limited PATH - launch from Terminal instead
- Check daemon is running: `ps aux | grep hld`
- Check MCP logs in Claude Code for connection errors
## Quick Start for Frontend Development
Always use React hooks, never the daemon client directly:

View File

@@ -14,9 +14,10 @@
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-tooltip": "^1.2.7",
"@tailwindcss/vite": "^4.1.11",
"@tauri-apps/api": "^2",
"@tauri-apps/api": "^2.7.0",
"@tauri-apps/plugin-clipboard-manager": "~2",
"@tauri-apps/plugin-fs": "~2",
"@tauri-apps/plugin-log": "^2.6.0",
"@tauri-apps/plugin-notification": "^2.3.0",
"@tauri-apps/plugin-opener": "^2",
"class-variance-authority": "^0.7.1",
@@ -373,7 +374,7 @@
"@tailwindcss/vite": ["@tailwindcss/vite@4.1.11", "", { "dependencies": { "@tailwindcss/node": "4.1.11", "@tailwindcss/oxide": "4.1.11", "tailwindcss": "4.1.11" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-RHYhrR3hku0MJFRV+fN2gNbDNEh3dwKvY8XJvTxCSXeMOsCRSr+uKvDWQcbizrHgjML6ZmTE5OwMrl5wKcujCw=="],
"@tauri-apps/api": ["@tauri-apps/api@2.5.0", "", {}, "sha512-Ldux4ip+HGAcPUmuLT8EIkk6yafl5vK0P0c0byzAKzxJh7vxelVtdPONjfgTm96PbN24yjZNESY8CKo8qniluA=="],
"@tauri-apps/api": ["@tauri-apps/api@2.7.0", "", {}, "sha512-v7fVE8jqBl8xJFOcBafDzXFc8FnicoH3j8o8DNNs0tHuEBmXUDqrCOAzMRX0UkfpwqZLqvrvK0GNQ45DfnoVDg=="],
"@tauri-apps/cli": ["@tauri-apps/cli@2.5.0", "", { "optionalDependencies": { "@tauri-apps/cli-darwin-arm64": "2.5.0", "@tauri-apps/cli-darwin-x64": "2.5.0", "@tauri-apps/cli-linux-arm-gnueabihf": "2.5.0", "@tauri-apps/cli-linux-arm64-gnu": "2.5.0", "@tauri-apps/cli-linux-arm64-musl": "2.5.0", "@tauri-apps/cli-linux-riscv64-gnu": "2.5.0", "@tauri-apps/cli-linux-x64-gnu": "2.5.0", "@tauri-apps/cli-linux-x64-musl": "2.5.0", "@tauri-apps/cli-win32-arm64-msvc": "2.5.0", "@tauri-apps/cli-win32-ia32-msvc": "2.5.0", "@tauri-apps/cli-win32-x64-msvc": "2.5.0" }, "bin": { "tauri": "tauri.js" } }, "sha512-rAtHqG0Gh/IWLjN2zTf3nZqYqbo81oMbqop56rGTjrlWk9pTTAjkqOjSL9XQLIMZ3RbeVjveCqqCA0s8RnLdMg=="],
@@ -403,6 +404,8 @@
"@tauri-apps/plugin-fs": ["@tauri-apps/plugin-fs@2.4.0", "", { "dependencies": { "@tauri-apps/api": "^2.6.0" } }, "sha512-Sp8AdDcbyXyk6LD6Pmdx44SH3LPeNAvxR2TFfq/8CwqzfO1yOyV+RzT8fov0NNN7d9nvW7O7MtMAptJ42YXA5g=="],
"@tauri-apps/plugin-log": ["@tauri-apps/plugin-log@2.6.0", "", { "dependencies": { "@tauri-apps/api": "^2.6.0" } }, "sha512-gVp3l31akA1Jk2bZsTA0hMFD5/gLe49Nw1btu5lViau0QqgC2XyT79LSwvy7a44ewtQbSexchqIg7oTJKMIbXQ=="],
"@tauri-apps/plugin-notification": ["@tauri-apps/plugin-notification@2.3.0", "", { "dependencies": { "@tauri-apps/api": "^2.6.0" } }, "sha512-QDwXo9VzAlH97c0veuf19TZI6cRBPfJDl2O6hNEDvI66j60lOO9z+PL6MJrj8A6Y+t55r7mGhe3rQWLmOre2HA=="],
"@tauri-apps/plugin-opener": ["@tauri-apps/plugin-opener@2.3.0", "", { "dependencies": { "@tauri-apps/api": "^2.0.0" } }, "sha512-yAbauwp8BCHIhhA48NN8rEf6OtfZBPCgTOCa10gmtoVCpmic5Bq+1Ba7C+NZOjogedkSiV7hAotjYnnbUVmYrw=="],
@@ -1323,6 +1326,8 @@
"@tauri-apps/plugin-notification/@tauri-apps/api": ["@tauri-apps/api@2.6.0", "", {}, "sha512-hRNcdercfgpzgFrMXWwNDBN0B7vNzOzRepy6ZAmhxi5mDLVPNrTpo9MGg2tN/F7JRugj4d2aF7E1rtPXAHaetg=="],
"@tauri-apps/plugin-opener/@tauri-apps/api": ["@tauri-apps/api@2.5.0", "", {}, "sha512-Ldux4ip+HGAcPUmuLT8EIkk6yafl5vK0P0c0byzAKzxJh7vxelVtdPONjfgTm96PbN24yjZNESY8CKo8qniluA=="],
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],

View File

@@ -16,6 +16,7 @@
"test": "bun test"
},
"dependencies": {
"@humanlayer/hld-sdk": "file:../hld/sdk/typescript",
"@radix-ui/react-collapsible": "^1.1.11",
"@radix-ui/react-dialog": "^1.1.14",
"@radix-ui/react-dropdown-menu": "^2.1.15",
@@ -26,12 +27,12 @@
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-tooltip": "^1.2.7",
"@tailwindcss/vite": "^4.1.11",
"@tauri-apps/api": "^2",
"@tauri-apps/api": "^2.7.0",
"@tauri-apps/plugin-clipboard-manager": "~2",
"@tauri-apps/plugin-fs": "~2",
"@tauri-apps/plugin-log": "^2.6.0",
"@tauri-apps/plugin-notification": "^2.3.0",
"@tauri-apps/plugin-opener": "^2",
"@humanlayer/hld-sdk": "file:../hld/sdk/typescript",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",

File diff suppressed because it is too large Load Diff

View File

@@ -23,7 +23,14 @@ tauri-plugin-opener = "2"
tauri-plugin-notification = "2.3.0"
tauri-plugin-fs = "2"
tauri-plugin-clipboard-manager = "2"
tauri-plugin-shell = "2"
tauri-plugin-store = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tracing = "0.1"
tracing-subscriber = "0.3"
log = "0.4"
tauri-plugin-log = "2"
tokio = { version = "1", features = ["full"] }
dirs = "5.0"
regex = "1.10"
reqwest = { version = "0.11", features = ["blocking"] }
chrono = { version = "0.4", features = ["serde"] }

View File

@@ -18,6 +18,11 @@
]
},
"clipboard-manager:default",
"clipboard-manager:allow-write-text"
"clipboard-manager:allow-write-text",
"shell:allow-spawn",
"shell:allow-execute",
"shell:allow-kill",
"store:default",
"log:default"
]
}

View File

@@ -0,0 +1,491 @@
use crate::get_branch_id;
use serde::{Deserialize, Serialize};
use std::env;
use std::fs;
use std::io::{BufRead, BufReader};
use std::path::PathBuf;
use std::process::{Child, Command, Stdio};
use std::sync::{Arc, Mutex};
use tauri::{AppHandle, Manager};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DaemonInfo {
pub port: u16,
pub pid: u32,
pub database_path: String,
pub socket_path: String,
pub branch_id: String,
pub is_running: bool,
}
#[derive(Clone)]
pub struct DaemonManager {
process: Arc<Mutex<Option<Child>>>,
info: Arc<Mutex<Option<DaemonInfo>>>,
}
impl DaemonManager {
pub fn new() -> Self {
Self {
process: Arc::new(Mutex::new(None)),
info: Arc::new(Mutex::new(None)),
}
}
pub async fn start_daemon(
&self,
app_handle: &AppHandle,
is_dev: bool,
branch_override: Option<String>,
) -> Result<DaemonInfo, String> {
// Check if already running
{
let process = self.process.lock().unwrap();
if process.is_some() {
if let Some(info) = self.info.lock().unwrap().as_ref() {
return Ok(info.clone());
}
}
}
// Check if daemon is already running on a specific port
if let Ok(port_str) = env::var("HUMANLAYER_DAEMON_HTTP_PORT") {
if let Ok(port) = port_str.parse::<u16>() {
// Try to connect to existing daemon
if check_daemon_health(port).await.is_ok() {
let info = DaemonInfo {
port,
pid: 0, // Unknown PID for external daemon
database_path: "external".to_string(),
socket_path: "external".to_string(),
branch_id: "external".to_string(),
is_running: true,
};
*self.info.lock().unwrap() = Some(info.clone());
return Ok(info);
}
}
}
// Determine branch identifier using shared function
let branch_id = get_branch_id(is_dev, branch_override);
// Set up paths
let home_dir = dirs::home_dir().ok_or("Failed to get home directory")?;
let humanlayer_dir = home_dir.join(".humanlayer");
fs::create_dir_all(&humanlayer_dir)
.map_err(|e| format!("Failed to create .humanlayer directory: {e}"))?;
// Database path
let database_path = if is_dev {
// Copy dev database if it doesn't exist
let dev_db = humanlayer_dir.join(format!("daemon-{branch_id}.db"));
if !dev_db.exists() {
let source_db = humanlayer_dir.join("daemon-dev.db");
if source_db.exists() {
fs::copy(&source_db, &dev_db)
.map_err(|e| format!("Failed to copy dev database: {e}"))?;
}
}
dev_db
} else {
humanlayer_dir.join("daemon.db")
};
// Socket path
let socket_path = if is_dev {
humanlayer_dir.join(format!("daemon-{branch_id}.sock"))
} else {
humanlayer_dir.join("daemon.sock")
};
// Get daemon binary path (macOS only)
let daemon_path = get_daemon_path(app_handle, is_dev)?;
// Build environment with port 0 for dynamic allocation
let mut env_vars = env::vars().collect::<Vec<_>>();
env_vars.push((
"HUMANLAYER_DATABASE_PATH".to_string(),
database_path.to_str().unwrap().to_string(),
));
env_vars.push((
"HUMANLAYER_DAEMON_SOCKET".to_string(),
socket_path.to_str().unwrap().to_string(),
));
env_vars.push(("HUMANLAYER_DAEMON_HTTP_PORT".to_string(), "0".to_string()));
env_vars.push((
"HUMANLAYER_DAEMON_HTTP_HOST".to_string(),
"localhost".to_string(),
));
if is_dev {
env_vars.push((
"HUMANLAYER_DAEMON_VERSION_OVERRIDE".to_string(),
branch_id.clone(),
));
// Enable debug logging for daemon in dev mode
env_vars.push((
"HUMANLAYER_DEBUG".to_string(),
"true".to_string(),
));
env_vars.push((
"GIN_MODE".to_string(),
"debug".to_string(),
));
}
// Start daemon with stdout capture and stderr logging
let mut cmd = Command::new(&daemon_path);
// Log the full command being executed for debugging
log::info!("[Tauri] Executing daemon at path: {daemon_path:?}");
log::info!("[Tauri] Daemon environment: database_path={}, socket_path={}, port=0, branch_id={branch_id}",
database_path.display(),
socket_path.display());
// Always capture stderr for better debugging, even in production
cmd.envs(env_vars)
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let mut child = cmd
.spawn()
.map_err(|e| format!("Failed to start daemon: {e}"))?;
// Get the PID before we do anything else
let pid = child.id();
log::info!("[Tauri] Daemon spawned with PID: {pid}");
// Always spawn a task to read stderr for debugging
if let Some(stderr) = child.stderr.take() {
let branch_id_clone = branch_id.clone();
let is_prod = !is_dev;
tokio::spawn(async move {
let reader = BufReader::new(stderr);
for line in reader.lines() {
match line {
Ok(line) => {
// In production, always log at info level or higher for visibility
if is_prod {
// Check if it's an error or warning
if line.contains("ERROR") || line.contains("error") || line.contains("Error") {
log::error!("[Daemon] {branch_id_clone}: {line}");
} else if line.contains("WARN") || line.contains("warn") || line.contains("Warning") {
log::warn!("[Daemon] {branch_id_clone}: {line}");
} else {
log::info!("[Daemon] {branch_id_clone}: {line}");
}
} else {
// Dev mode: parse log levels as before
let level = extract_log_level(&line);
let cleaned_line = remove_timestamp(&line);
match level {
LogLevel::Error => log::error!("[Daemon] {branch_id_clone}: {cleaned_line}"),
LogLevel::Warn => log::warn!("[Daemon] {branch_id_clone}: {cleaned_line}"),
LogLevel::Info => log::info!("[Daemon] {branch_id_clone}: {cleaned_line}"),
LogLevel::Debug => log::debug!("[Daemon] {branch_id_clone}: {cleaned_line}"),
LogLevel::Trace => log::trace!("[Daemon] {branch_id_clone}: {cleaned_line}"),
}
}
}
Err(e) => {
log::error!("[Tauri] Error reading daemon stderr: {e}");
break;
}
}
}
log::info!("[Tauri] Daemon stderr reader finished for {branch_id_clone}");
});
}
// Parse stdout to get the actual port
let stdout = child
.stdout
.take()
.ok_or("Failed to capture daemon stdout")?;
let mut reader = BufReader::new(stdout);
let mut actual_port = None;
let mut first_line = String::new();
// Read the first line synchronously to get the port
log::info!("[Tauri] Waiting for daemon to report port on stdout...");
match reader.read_line(&mut first_line) {
Ok(0) => {
log::error!("[Tauri] Daemon stdout closed immediately (0 bytes read)");
return Err("Daemon stdout closed before reporting port".to_string());
}
Ok(n) => {
log::info!("[Tauri] Read {n} bytes from daemon stdout: {first_line:?}");
}
Err(e) => {
log::error!("[Tauri] Failed to read daemon stdout: {e}");
return Err(format!("Failed to read daemon stdout: {e}"));
}
}
if first_line.starts_with("HTTP_PORT=") {
let port_str = first_line.trim().replace("HTTP_PORT=", "");
log::info!("[Tauri] Attempting to parse port from: {port_str:?}");
match port_str.parse::<u16>() {
Ok(p) => {
actual_port = Some(p);
log::info!("[Tauri] Successfully parsed port: {p}");
}
Err(e) => {
log::error!("[Tauri] Failed to parse port from {port_str:?}: {e}");
}
}
} else {
log::error!("[Tauri] First line from daemon stdout doesn't start with HTTP_PORT=: {first_line:?}");
}
let port = actual_port.ok_or_else(|| {
format!("Daemon failed to report port. First line was: {first_line:?}")
})?;
log::info!("[Tauri] Got port {port} from daemon stdout");
// Now spawn a task to keep reading stdout to prevent SIGPIPE
let branch_id_for_stdout = branch_id.clone();
tokio::spawn(async move {
// Continue reading the rest of stdout
for line in reader.lines() {
match line {
Ok(line) => {
log::trace!("[Tauri] Daemon stdout {branch_id_for_stdout}: {line}");
}
Err(e) => {
log::debug!("[Tauri] Daemon stdout closed for {branch_id_for_stdout}: {e}");
break;
}
}
}
log::debug!("[Tauri] Stdout reader task finished for {branch_id_for_stdout}");
});
// Check if process is still alive after reading port
match child.try_wait() {
Ok(None) => log::info!("[Tauri] Daemon process still running after port read"),
Ok(Some(status)) => {
return Err(format!("Daemon process exited immediately after starting! Status: {status:?}"));
}
Err(e) => log::error!("[Tauri] Error checking daemon status: {e}"),
}
let daemon_info = DaemonInfo {
port,
pid,
database_path: database_path.to_str().unwrap().to_string(),
socket_path: socket_path.to_str().unwrap().to_string(),
branch_id: branch_id.clone(),
is_running: true,
};
// Store the process and info before awaiting
{
let mut process = self.process.lock().unwrap();
*process = Some(child);
}
*self.info.lock().unwrap() = Some(daemon_info.clone());
// Wait for daemon to be ready
log::info!("[Tauri] Waiting for daemon to be ready on port {port}");
wait_for_daemon(port).await?;
log::info!("[Tauri] Daemon is ready and responding to health checks");
// Spawn a task to monitor the daemon process
let process_arc = self.process.clone();
let branch_id_clone = branch_id.clone();
let port_for_monitor = port;
tokio::spawn(async move {
let mut last_check = std::time::Instant::now();
loop {
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
let mut process_guard = process_arc.lock().unwrap();
if let Some(child) = process_guard.as_mut() {
match child.try_wait() {
Ok(Some(status)) => {
// Process has exited
log::error!(
"[Tauri] Daemon process exited unexpectedly! Branch: {branch_id_clone}, Port: {port_for_monitor}, Exit status: {status:?}, Time since last check: {:?}",
last_check.elapsed()
);
// Clear the process
*process_guard = None;
break;
}
Ok(None) => {
// Still running
last_check = std::time::Instant::now();
}
Err(e) => {
log::error!("[Tauri] Error checking daemon process status: {e}");
break;
}
}
} else {
// No process to monitor
break;
}
}
});
Ok(daemon_info)
}
pub fn stop_daemon(&self) -> Result<(), String> {
let mut process = self.process.lock().unwrap();
if let Some(mut child) = process.take() {
child
.kill()
.map_err(|e| format!("Failed to stop daemon: {e}"))?;
// Wait for process to exit
let _ = child.wait();
// Update store to mark daemon as not running
if let Some(info) = self.info.lock().unwrap().as_mut() {
info.is_running = false;
}
}
Ok(())
}
pub fn get_info(&self) -> Option<DaemonInfo> {
self.info.lock().unwrap().clone()
}
pub fn is_running(&self) -> bool {
let mut process = self.process.lock().unwrap();
if let Some(child) = process.as_mut() {
// Check if process is still alive
match child.try_wait() {
Ok(None) => true, // Still running
_ => false, // Exited or error
}
} else {
false
}
}
}
fn get_daemon_path(app_handle: &AppHandle, is_dev: bool) -> Result<PathBuf, String> {
if is_dev {
// In dev mode, look for hld-dev in the project
let current =
env::current_dir().map_err(|e| format!("Failed to get current directory: {e}"))?;
// Handle both running from src-tauri and from humanlayer-wui
let dev_path = if current.ends_with("src-tauri") {
current
.parent() // humanlayer-wui
.and_then(|p| p.parent()) // humanlayer root
.ok_or("Failed to get parent directory")?
.join("hld")
.join("hld-dev")
} else {
current
.parent() // Go up from humanlayer-wui to humanlayer
.ok_or("Failed to get parent directory")?
.join("hld")
.join("hld-dev")
};
if dev_path.exists() {
Ok(dev_path)
} else {
Err(format!(
"Development daemon not found at {dev_path:?}. Run 'make daemon-dev-build' first."
))
}
} else {
// In production, use bundled binary
let resource_dir = app_handle
.path()
.resource_dir()
.map_err(|e| format!("Failed to get resource directory: {e}"))?;
Ok(resource_dir.join("bin").join("hld"))
}
}
async fn check_daemon_health(port: u16) -> Result<(), String> {
let client = reqwest::Client::new();
match client
.get(format!("http://localhost:{port}/api/v1/health"))
.send()
.await
{
Ok(response) if response.status().is_success() => Ok(()),
Ok(_) => Err("Daemon health check failed".to_string()),
Err(e) => Err(format!("Failed to connect to daemon: {e}")),
}
}
async fn wait_for_daemon(port: u16) -> Result<(), String> {
let start = std::time::Instant::now();
let timeout = std::time::Duration::from_secs(10);
while start.elapsed() < timeout {
if check_daemon_health(port).await.is_ok() {
return Ok(());
}
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
}
Err("Daemon failed to start within 10 seconds".to_string())
}
#[derive(Debug)]
enum LogLevel {
Error,
Warn,
Info,
Debug,
Trace,
}
fn extract_log_level(line: &str) -> LogLevel {
// slog format includes level like: "2024-01-30T10:15:30Z INFO message"
// or sometimes: "INFO[0001] message" for other loggers
if line.contains(" ERROR ") || line.contains("ERROR[") {
LogLevel::Error
} else if line.contains(" WARN ") || line.contains("WARN[") {
LogLevel::Warn
} else if line.contains(" INFO ") || line.contains("INFO[") {
LogLevel::Info
} else if line.contains(" DEBUG ") || line.contains("DEBUG[") {
LogLevel::Debug
} else if line.contains(" TRACE ") || line.contains("TRACE[") {
LogLevel::Trace
} else {
// Default to info for unparseable lines
LogLevel::Info
}
}
fn remove_timestamp(line: &str) -> &str {
// Remove slog timestamp to avoid double timestamps
// Format: "2024-01-30T10:15:30Z LEVEL message"
if let Some(idx) = line.find(" INFO ") {
&line[idx + 6..]
} else if let Some(idx) = line.find(" ERROR ") {
&line[idx + 7..]
} else if let Some(idx) = line.find(" WARN ") {
&line[idx + 6..]
} else if let Some(idx) = line.find(" DEBUG ") {
&line[idx + 7..]
} else if let Some(idx) = line.find(" TRACE ") {
&line[idx + 7..]
} else {
// Return original if no timestamp found
line
}
}

View File

@@ -1,13 +1,305 @@
mod daemon;
use daemon::{DaemonInfo, DaemonManager};
use std::env;
use std::path::PathBuf;
use std::process::Command;
use tauri::{Manager, State};
use tauri_plugin_store::StoreExt;
// Branch detection utilities
pub fn get_git_branch() -> Option<String> {
Command::new("git")
.args(["branch", "--show-current"])
.output()
.ok()
.and_then(|output| {
if output.status.success() {
String::from_utf8(output.stdout)
.ok()
.map(|s| s.trim().to_string())
} else {
None
}
})
}
pub fn extract_ticket_id(branch: &str) -> Option<String> {
// Extract patterns like "eng-1234" from branch names
let re = regex::Regex::new(r"(eng-\d+)").ok()?;
re.captures(branch)
.and_then(|cap| cap.get(1))
.map(|m| m.as_str().to_string())
}
pub fn get_branch_id(is_dev: bool, branch_override: Option<String>) -> String {
if is_dev {
let branch = branch_override
.or_else(get_git_branch)
.unwrap_or_else(|| "dev".to_string());
extract_ticket_id(&branch).unwrap_or(branch)
} else {
"production".to_string()
}
}
// Helper to get store path based on dev mode and branch
fn get_store_path(is_dev: bool, branch_id: Option<&str>) -> PathBuf {
let home = dirs::home_dir().expect("Failed to get home directory");
let humanlayer_dir = home.join(".humanlayer");
if is_dev {
// Use branch-specific store in dev mode
if let Some(branch) = branch_id {
humanlayer_dir.join(format!("codelayer-{branch}.json"))
} else {
humanlayer_dir.join("codelayer-dev.json")
}
} else {
humanlayer_dir.join("codelayer.json")
}
}
#[tauri::command]
async fn start_daemon(
app_handle: tauri::AppHandle,
daemon_manager: State<'_, DaemonManager>,
is_dev: bool,
branch_override: Option<String>,
) -> Result<DaemonInfo, String> {
let info = daemon_manager
.start_daemon(&app_handle, is_dev, branch_override)
.await?;
// Save to store using branch-specific path in dev mode
let store_path = get_store_path(is_dev, Some(&info.branch_id));
let store = app_handle
.store(&store_path)
.map_err(|e| format!("Failed to access store: {e}"))?;
store.set("current_daemon", serde_json::to_value(&info).unwrap());
store
.save()
.map_err(|e| format!("Failed to save store: {e}"))?;
Ok(info)
}
#[tauri::command]
async fn stop_daemon(
app_handle: tauri::AppHandle,
daemon_manager: State<'_, DaemonManager>,
) -> Result<(), String> {
// Get daemon info before stopping to know which store to update
if let Some(info) = daemon_manager.get_info() {
let is_dev = info.branch_id != "production";
let store_path = get_store_path(is_dev, Some(&info.branch_id));
// Stop the daemon
daemon_manager.stop_daemon()?;
// Update store to mark as not running
let store = app_handle
.store(&store_path)
.map_err(|e| format!("Failed to access store: {e}"))?;
// Get the stored daemon info and update is_running
if let Some(mut stored_info) = store
.get("current_daemon")
.and_then(|v| serde_json::from_value::<DaemonInfo>(v).ok())
{
stored_info.is_running = false;
store.set(
"current_daemon",
serde_json::to_value(&stored_info).unwrap(),
);
store
.save()
.map_err(|e| format!("Failed to save store: {e}"))?;
}
} else {
daemon_manager.stop_daemon()?;
}
Ok(())
}
#[tauri::command]
async fn get_daemon_info(
app_handle: tauri::AppHandle,
daemon_manager: State<'_, DaemonManager>,
is_dev: bool,
) -> Result<Option<DaemonInfo>, String> {
// First check if daemon manager has info
if let Some(info) = daemon_manager.get_info() {
return Ok(Some(info));
}
// Otherwise check store for last known daemon
let store_path = get_store_path(is_dev, None);
let store = app_handle
.store(&store_path)
.map_err(|e| format!("Failed to access store: {e}"))?;
let stored_info = store
.get("current_daemon")
.and_then(|v| serde_json::from_value::<DaemonInfo>(v).ok());
Ok(stored_info)
}
#[tauri::command]
async fn is_daemon_running(daemon_manager: State<'_, DaemonManager>) -> Result<bool, String> {
Ok(daemon_manager.is_running())
}
#[tauri::command]
async fn get_log_directory() -> Result<String, String> {
let is_dev = cfg!(debug_assertions);
if is_dev {
// Dev mode: return branch-based folder
let branch_id = get_branch_id(is_dev, None);
let home = dirs::home_dir().ok_or("Failed to get home directory")?;
let log_dir = home.join(".humanlayer").join("logs").join(format!("wui-{branch_id}"));
Ok(log_dir.to_string_lossy().to_string())
} else {
// Production: use tauri API to get platform-specific log directory
// This will be handled by the frontend using appLogDir()
Err("Use appLogDir() for production".to_string())
}
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
// Initialize tracing
tracing_subscriber::fmt::init();
tauri::Builder::default()
.plugin(tauri_plugin_clipboard_manager::init())
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_notification::init())
.plugin(tauri_plugin_clipboard_manager::init())
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_store::Builder::new().build())
.plugin({
let is_dev = cfg!(debug_assertions);
let branch_id = get_branch_id(is_dev, None);
// Determine log directory based on dev/prod mode
let log_targets = if is_dev {
// Dev mode: use branch-based folder in ~/.humanlayer/logs/
let home = dirs::home_dir().expect("Failed to get home directory");
let log_dir = home.join(".humanlayer").join("logs").join(format!("wui-{branch_id}"));
// Create the directory if it doesn't exist
std::fs::create_dir_all(&log_dir).ok();
// Store the log directory path in app state for frontend access
println!("[WUI] Logs will be written to: {}", log_dir.display());
vec![
tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::Folder {
path: log_dir,
file_name: Some("codelayer.log".into()),
}),
tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::Stdout),
tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::Webview),
]
} else {
// Production: use default platform-specific log directory
vec![
tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::LogDir {
file_name: None,
}),
tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::Webview),
]
};
tauri_plugin_log::Builder::new()
.targets(log_targets)
.level(if is_dev {
log::LevelFilter::Debug
} else {
log::LevelFilter::Info
})
.max_file_size(50_000_000) // 50MB before rotation
.rotation_strategy(tauri_plugin_log::RotationStrategy::KeepAll)
.build()
})
.setup(|app| {
let daemon_manager = DaemonManager::new();
app.manage(daemon_manager.clone());
// Check if auto-launch is disabled
let should_autolaunch = env::var("HUMANLAYER_WUI_AUTOLAUNCH_DAEMON")
.map(|v| v.to_lowercase() != "false")
.unwrap_or(true);
if should_autolaunch {
// Start daemon automatically
let app_handle_clone = app.app_handle().clone();
tauri::async_runtime::spawn(async move {
let is_dev = cfg!(debug_assertions);
// Small delay to ensure UI is ready
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
// Try to start daemon, but don't show errors in UI
match daemon_manager
.start_daemon(&app_handle_clone, is_dev, None)
.await
{
Ok(info) => {
log::info!("[Tauri] Daemon started automatically on port {}", info.port);
}
Err(e) => {
// Log error but don't interrupt user experience
log::error!("[Tauri] Failed to auto-start daemon: {e}");
}
}
});
} else {
log::info!("[Tauri] Daemon auto-launch disabled by environment variable");
}
Ok(())
})
.on_window_event(|window, event| {
if let tauri::WindowEvent::CloseRequested { .. } = event {
let daemon_manager = window.state::<DaemonManager>();
// Always stop daemon when window closes
if let Some(info) = daemon_manager.get_info() {
let is_dev = info.branch_id != "production";
let store_path = get_store_path(is_dev, Some(&info.branch_id));
if let Ok(store) = window.app_handle().store(&store_path) {
// Update store to mark daemon as not running
if let Some(mut stored_info) = store
.get("current_daemon")
.and_then(|v| serde_json::from_value::<DaemonInfo>(v).ok())
{
stored_info.is_running = false;
store.set(
"current_daemon",
serde_json::to_value(&stored_info).unwrap(),
);
let _ = store.save();
}
}
// Stop the daemon
let _ = daemon_manager.stop_daemon();
}
}
})
.invoke_handler(tauri::generate_handler![
start_daemon,
stop_daemon,
get_daemon_info,
is_daemon_running,
get_log_directory,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

View File

@@ -31,7 +31,8 @@
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
],
"resources": ["bin/hld", "bin/humanlayer"]
},
"plugins": {
"fs": {

View File

@@ -2,6 +2,7 @@ import type { Session, SessionStatus } from '@/lib/daemon/types'
import { ViewMode } from '@/lib/daemon/types'
import { create } from 'zustand'
import { daemonClient } from '@/lib/daemon'
import { logger } from '@/lib/logging'
interface StoreState {
/* Sessions */
@@ -111,7 +112,7 @@ export const useStore = create<StoreState>((set, get) => ({
})
set({ sessions: response.sessions })
} catch (error) {
console.error('Failed to refresh sessions:', error)
logger.error('Failed to refresh sessions:', error)
}
},
refreshActiveSessionConversation: async (sessionId: string) => {
@@ -124,7 +125,7 @@ export const useStore = create<StoreState>((set, get) => ({
const conversationResponse = await daemonClient.getConversation({ session_id: sessionId })
updateActiveSessionConversation(conversationResponse)
} catch (error) {
console.error('Failed to refresh active session conversation:', error)
logger.error('Failed to refresh active session conversation:', error)
}
}
},
@@ -164,7 +165,7 @@ export const useStore = create<StoreState>((set, get) => ({
await daemonClient.interruptSession(sessionId)
// The session status will be updated via the subscription
} catch (error) {
console.error('Failed to interrupt session:', error)
logger.error('Failed to interrupt session:', error)
}
},
archiveSession: async (sessionId: string, archived: boolean) => {
@@ -175,7 +176,7 @@ export const useStore = create<StoreState>((set, get) => ({
// Refresh sessions to update the list based on current view mode
await get().refreshSessions()
} catch (error) {
console.error('Failed to archive session:', error)
logger.error('Failed to archive session:', error)
throw error
}
},
@@ -183,14 +184,14 @@ export const useStore = create<StoreState>((set, get) => ({
try {
const response = await daemonClient.bulkArchiveSessions({ session_ids: sessionIds, archived })
if (!response.success) {
console.error('Failed to archive sessions')
logger.error('Failed to archive sessions')
}
// Refresh sessions to update the list
await get().refreshSessions()
// Clear selection after bulk operation
get().clearSelection()
} catch (error) {
console.error('Failed to bulk archive sessions:', error)
logger.error('Failed to bulk archive sessions:', error)
throw error
}
},
@@ -216,7 +217,7 @@ export const useStore = create<StoreState>((set, get) => ({
const anchorIndex = sessions.findIndex(s => s.id === anchorId)
const targetIndex = sessions.findIndex(s => s.id === targetId)
console.log('[Store] selectRange (REPLACE):', {
logger.log('[Store] selectRange (REPLACE):', {
anchorId,
targetId,
anchorIndex,
@@ -238,7 +239,7 @@ export const useStore = create<StoreState>((set, get) => ({
newSelection.add(sessions[i].id)
}
console.log('[Store] selectRange result:', {
logger.log('[Store] selectRange result:', {
newSelectionSize: newSelection.size,
newSelectionIds: Array.from(newSelection),
})
@@ -251,7 +252,7 @@ export const useStore = create<StoreState>((set, get) => ({
const anchorIndex = sessions.findIndex(s => s.id === anchorId)
const targetIndex = sessions.findIndex(s => s.id === targetId)
console.log('[Store] addRangeToSelection (ADD):', {
logger.log('[Store] addRangeToSelection (ADD):', {
anchorId,
targetId,
anchorIndex,
@@ -276,7 +277,7 @@ export const useStore = create<StoreState>((set, get) => ({
newSelection.add(sessions[i].id)
}
console.log('[Store] addRangeToSelection result:', {
logger.log('[Store] addRangeToSelection result:', {
newSelectionSize: newSelection.size,
newSelectionIds: Array.from(newSelection),
})
@@ -289,7 +290,7 @@ export const useStore = create<StoreState>((set, get) => ({
const anchorIndex = sessions.findIndex(s => s.id === anchorId)
const targetIndex = sessions.findIndex(s => s.id === targetId)
console.log('[Store] updateCurrentRange:', {
logger.log('[Store] updateCurrentRange:', {
anchorId,
targetId,
anchorIndex,
@@ -344,7 +345,7 @@ export const useStore = create<StoreState>((set, get) => ({
newSelection.add(sessions[i].id)
}
console.log('[Store] updateCurrentRange result:', {
logger.log('[Store] updateCurrentRange result:', {
newSelectionSize: newSelection.size,
newSelectionIds: Array.from(newSelection),
currentRange: `${rangeStart}-${rangeEnd}`,
@@ -371,7 +372,7 @@ export const useStore = create<StoreState>((set, get) => ({
// Check if we're starting within an existing selection
const isStartingInSelection = selectedSessions.has(sessionId)
console.log(
logger.log(
`[bulkSelect] sessionId: ${sessionId}, direction: ${direction}, isStartingInSelection: ${isStartingInSelection}`,
)
@@ -418,7 +419,7 @@ export const useStore = create<StoreState>((set, get) => ({
const anchorId = sessions[anchorIndex].id
console.log(
logger.log(
`[bulkSelect] Starting in selection, found range: ${rangeStart}-${rangeEnd}, anchor at ${anchorIndex}`,
)
@@ -426,11 +427,11 @@ export const useStore = create<StoreState>((set, get) => ({
state.updateCurrentRange(anchorId, targetSession.id)
} else if (selectedSessions.size > 0 && !isStartingInSelection) {
// We have selections but starting fresh - add to existing
console.log('[bulkSelect] Adding new range to existing selections')
logger.log('[bulkSelect] Adding new range to existing selections')
state.addRangeToSelection(sessionId, targetSession.id)
} else {
// No selections or replacing - create new range
console.log('[bulkSelect] Creating new selection range')
logger.log('[bulkSelect] Creating new selection range')
state.selectRange(sessionId, targetSession.id)
}
@@ -472,7 +473,7 @@ export const useStore = create<StoreState>((set, get) => ({
const newMap = new Map(state.recentNavigations)
const timestamp = Date.now()
newMap.set(sessionId, timestamp)
console.log(
logger.log(
`Tracking navigation from session ${sessionId} at ${new Date(timestamp).toISOString()}`,
)
@@ -483,7 +484,7 @@ export const useStore = create<StoreState>((set, get) => ({
const updatedMap = new Map(currentMap)
updatedMap.delete(sessionId)
set({ recentNavigations: updatedMap })
console.log(`Removed navigation tracking for session ${sessionId} after timeout`)
logger.log(`Removed navigation tracking for session ${sessionId} after timeout`)
}
}, 5000)
return { recentNavigations: newMap }
@@ -493,13 +494,13 @@ export const useStore = create<StoreState>((set, get) => ({
const now = Date.now()
if (!timestamp) {
console.log(`No navigation tracking found for session ${sessionId}`)
logger.log(`No navigation tracking found for session ${sessionId}`)
return false
}
const elapsed = now - timestamp
const wasRecent = elapsed < withinMs
console.log(
logger.log(
`Checking navigation for session ${sessionId}: elapsed ${elapsed}ms, within ${withinMs}ms window: ${wasRecent}`,
)
return wasRecent
@@ -554,7 +555,7 @@ export const useStore = create<StoreState>((set, get) => ({
})
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Failed to fetch session detail'
console.error('Failed to fetch session detail:', error)
logger.error('Failed to fetch session detail:', error)
set({
activeSessionDetail: {

View File

@@ -0,0 +1,224 @@
import { useState, useEffect } from 'react'
import { daemonService, type DaemonInfo } from '@/services/daemon-service'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Loader2, RefreshCw, Link, Server } from 'lucide-react'
import { useDaemonConnection } from '@/hooks/useDaemonConnection'
import { logger } from '@/lib/logging'
interface DebugPanelProps {
open?: boolean
onOpenChange?: (open: boolean) => void
}
export function DebugPanel({ open, onOpenChange }: DebugPanelProps) {
const { connected, reconnect } = useDaemonConnection()
const [isRestarting, setIsRestarting] = useState(false)
const [restartError, setRestartError] = useState<string | null>(null)
const [customUrl, setCustomUrl] = useState('')
const [connectError, setConnectError] = useState<string | null>(null)
const [daemonType, setDaemonType] = useState<'managed' | 'external'>('managed')
const [externalDaemonUrl, setExternalDaemonUrl] = useState<string | null>(null)
const [daemonInfo, setDaemonInfo] = useState<DaemonInfo | null>(null)
// Only show in dev mode
if (!import.meta.env.DEV) {
return null
}
useEffect(() => {
loadDaemonInfo()
}, [connected])
async function loadDaemonInfo() {
try {
const info = await daemonService.getDaemonInfo()
setDaemonInfo(info)
const type = daemonService.getDaemonType()
setDaemonType(type)
if (type === 'external') {
setExternalDaemonUrl((window as any).__HUMANLAYER_DAEMON_URL || null)
}
} catch (error) {
logger.error('Failed to load daemon info:', error)
}
}
async function handleRestartDaemon() {
setIsRestarting(true)
setRestartError(null)
try {
// Stop current daemon
await daemonService.stopDaemon()
// Wait a moment
await new Promise(resolve => setTimeout(resolve, 1000))
// Start new daemon (with rebuild in dev)
await daemonService.startDaemon(true)
// Wait for it to be ready
await new Promise(resolve => setTimeout(resolve, 2000))
// Reconnect
await reconnect()
await loadDaemonInfo()
setIsRestarting(false)
} catch (error: any) {
setRestartError(error.message || 'Failed to restart daemon')
setIsRestarting(false)
}
}
async function handleConnectToCustom() {
setConnectError(null)
try {
await daemonService.connectToExisting(customUrl)
await reconnect()
await loadDaemonInfo()
setCustomUrl('')
} catch (error: any) {
setConnectError(error.message || 'Failed to connect')
}
}
async function handleSwitchToManaged() {
try {
await daemonService.switchToManagedDaemon()
await reconnect()
await loadDaemonInfo()
} catch (error: any) {
setConnectError(error.message || 'Failed to switch to managed daemon')
}
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Debug Panel</DialogTitle>
<DialogDescription>Advanced daemon management for developers</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<Card>
<CardHeader>
<CardTitle className="text-sm">Daemon Status</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm">Connection</span>
<span
className={`text-sm font-medium ${connected ? 'text-[var(--terminal-success)]' : 'text-[var(--terminal-error)]'}`}
>
{connected ? 'Connected' : 'Disconnected'}
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Daemon Type</span>
<div className="flex items-center gap-2">
<Server className="h-3 w-3 text-muted-foreground" />
<span className="text-sm font-medium capitalize">{daemonType}</span>
</div>
</div>
{daemonType === 'external' && externalDaemonUrl && (
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">External URL</span>
<span className="text-sm font-mono">{externalDaemonUrl}</span>
</div>
)}
{daemonInfo && (
<>
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Port</span>
<span className="text-sm font-mono">{daemonInfo.port}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Branch</span>
<span className="text-sm font-mono">{daemonInfo.branch_id}</span>
</div>
</>
)}
{daemonType === 'managed' ? (
<Button
onClick={handleRestartDaemon}
disabled={isRestarting}
className="w-full"
variant="outline"
>
{isRestarting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Restarting Daemon...
</>
) : (
<>
<RefreshCw className="mr-2 h-4 w-4" />
Restart Daemon
</>
)}
</Button>
) : (
<Button onClick={handleSwitchToManaged} className="w-full" variant="outline">
Switch to Managed Daemon
</Button>
)}
{restartError && <p className="text-sm text-destructive">{restartError}</p>}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-sm">Connect to Existing Daemon</CardTitle>
<CardDescription className="text-xs">
Connect to a daemon running on a custom URL
</CardDescription>
</CardHeader>
<CardContent className="space-y-2">
<div className="space-y-1">
<Label htmlFor="url" className="text-sm">
Daemon URL
</Label>
<Input
id="url"
type="text"
placeholder="http://127.0.0.1:7777"
value={customUrl}
onChange={e => setCustomUrl(e.target.value)}
/>
</div>
<Button
onClick={handleConnectToCustom}
disabled={!customUrl}
className="w-full"
variant="outline"
>
<Link className="mr-2 h-4 w-4" />
Connect
</Button>
{connectError && <p className="text-sm text-destructive">{connectError}</p>}
</CardContent>
</Card>
</div>
</DialogContent>
</Dialog>
)
}

View File

@@ -15,14 +15,18 @@ import { Toaster } from 'sonner'
import { notificationService } from '@/services/NotificationService'
import { useTheme } from '@/contexts/ThemeContext'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { MessageCircle } from 'lucide-react'
import { MessageCircle, Bug } from 'lucide-react'
import { openUrl } from '@tauri-apps/plugin-opener'
import { DebugPanel } from '@/components/DebugPanel'
import { notifyLogLocation } from '@/lib/log-notification'
import '@/App.css'
import { logger } from '@/lib/logging'
export function Layout() {
const [approvals, setApprovals] = useState<any[]>([])
const [activeSessionId] = useState<string | null>(null)
const { setTheme } = useTheme()
const [isDebugPanelOpen, setIsDebugPanelOpen] = useState(false)
// Use the daemon connection hook for all connection management
const { connected, connecting, version, connect } = useDaemonConnection()
@@ -46,18 +50,18 @@ export function Layout() {
// Set up single SSE subscription for all events
useSessionSubscriptions(connected, {
onSessionStatusChanged: async data => {
console.log('useSessionSubscriptions.onSessionStatusChanged', data)
logger.log('useSessionSubscriptions.onSessionStatusChanged', data)
const { session_id, new_status } = data
updateSessionStatus(session_id, new_status as SessionStatus)
await refreshActiveSessionConversation(session_id)
},
onNewApproval: async data => {
console.log('useSessionSubscriptions.onNewApproval', data)
logger.log('useSessionSubscriptions.onNewApproval', data)
updateSessionStatus(data.session_id, SessionStatus.WaitingInput)
await refreshActiveSessionConversation(data.session_id)
},
onApprovalResolved: async data => {
console.log('useSessionSubscriptions.onApprovalResolved', data)
logger.log('useSessionSubscriptions.onApprovalResolved', data)
updateSessionStatus(data.session_id, SessionStatus.Running)
await refreshActiveSessionConversation(data.session_id)
},
@@ -82,7 +86,7 @@ export function Layout() {
'https://github.com/humanlayer/humanlayer/issues/new?title=Feedback%20on%20CodeLayer&body=%23%23%23%20Problem%20to%20solve%20%2F%20Expected%20Behavior%0A%0A%0A%23%23%23%20Proposed%20solution',
)
} catch (error) {
console.error('Failed to open feedback URL:', error)
logger.error('Failed to open feedback URL:', error)
}
})
@@ -123,11 +127,18 @@ export function Layout() {
return () => document.removeEventListener('keydown', handleKeyDown)
}, [handleKeyDown])
// Notify about log location on startup (production only)
useEffect(() => {
if (!import.meta.env.DEV) {
notifyLogLocation()
}
}, [])
const loadSessions = async () => {
try {
await useStore.getState().refreshSessions()
} catch (error) {
console.error('Failed to load sessions:', error)
logger.error('Failed to load sessions:', error)
}
}
@@ -135,7 +146,7 @@ export function Layout() {
try {
// Handle new approval format directly
if (!approval || !approval.id) {
console.error('Invalid approval data:', approval)
logger.error('Invalid approval data:', approval)
return
}
@@ -240,6 +251,21 @@ export function Layout() {
<p>Submit feedback (F)</p>
</TooltipContent>
</Tooltip>
{import.meta.env.DEV && (
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={() => setIsDebugPanelOpen(true)}
className="px-1.5 py-0.5 text-xs font-mono border border-border bg-background text-foreground hover:bg-accent/10 transition-colors"
>
<Bug className="w-3 h-3" />
</button>
</TooltipTrigger>
<TooltipContent>
<p>Debug Panel (Dev Only)</p>
</TooltipContent>
</Tooltip>
)}
<ThemeSelector />
<div className="flex items-center gap-2 font-mono text-xs">
<span className="uppercase tracking-wider">
@@ -264,6 +290,9 @@ export function Layout() {
{/* Notifications */}
<Toaster position="bottom-right" richColors />
{/* Debug Panel */}
<DebugPanel open={isDebugPanelOpen} onOpenChange={setIsDebugPanelOpen} />
</div>
)
}

View File

@@ -1,4 +1,5 @@
import React from 'react'
import { logger } from '@/lib/logging'
interface Props {
children: React.ReactNode
@@ -21,7 +22,7 @@ export class MarkdownErrorBoundary extends React.Component<Props, State> {
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error('Markdown rendering error:', error, errorInfo)
logger.error('Markdown rendering error:', error, errorInfo)
}
render() {

View File

@@ -30,6 +30,7 @@ import { useSessionNavigation } from './hooks/useSessionNavigation'
import { useTaskGrouping } from './hooks/useTaskGrouping'
import { useSessionClipboard } from './hooks/useSessionClipboard'
import { useStealHotkeyScope } from '@/hooks/useStealHotkeyScope'
import { logger } from '@/lib/logging'
interface SessionDetailProps {
session: Session
@@ -307,7 +308,7 @@ function SessionDetail({ session, onClose }: SessionDetailProps) {
'escape',
ev => {
if ((ev.target as HTMLElement)?.dataset.slot === 'dialog-close') {
console.warn('Ignoring onClose triggered by dialog-close in SessionDetail')
logger.warn('Ignoring onClose triggered by dialog-close in SessionDetail')
return null
}
@@ -355,7 +356,7 @@ function SessionDetail({ session, onClose }: SessionDetailProps) {
// State will be updated via event subscription
} catch (error) {
console.error('Failed to toggle auto-accept mode:', error)
logger.error('Failed to toggle auto-accept mode:', error)
}
},
{

View File

@@ -1,4 +1,5 @@
import React, { Fragment } from 'react'
import { logger } from '@/lib/logging'
// --- Minimal diff utilities (no third-party libraries) ---
function computeLineDiff(oldStr: string, newStr: string) {
@@ -601,7 +602,7 @@ export const CustomDiffViewer = ({
)
} catch (error) {
// Log detailed context for debugging
console.warn('Snapshot-based diff rendering failed, falling back to simple diff:', {
logger.warn('Snapshot-based diff rendering failed, falling back to simple diff:', {
error: error instanceof Error ? error.message : String(error),
editsCount: edits.length,
fileContentLength: fileContents?.length,

View File

@@ -1,5 +1,6 @@
import React, { Component, ReactNode } from 'react'
import { Button } from '@/components/ui/button'
import { logger } from '@/lib/logging'
interface ErrorBoundaryProps {
children: ReactNode
@@ -21,7 +22,7 @@ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundarySt
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error('SessionDetail Error:', error, errorInfo)
logger.error('SessionDetail Error:', error, errorInfo)
}
render() {

View File

@@ -62,7 +62,7 @@ export function eventToDisplayObject(
let iconComponent = null
let toolResultContent = null
// console.log('event', event)
// logger.log('event', event)
// Check if this is a thinking message
const isThinking =
event.eventType === ConversationEventType.Thinking ||
@@ -618,7 +618,7 @@ export function eventToDisplayObject(
}
if (subject === null) {
// console.warn('Unknown subject for event', event)
// logger.warn('Unknown subject for event', event)
subject = <span>Unknown Subject</span>
}

View File

@@ -17,6 +17,7 @@ import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
import { daemonClient } from '@/lib/daemon/client'
import { renderSessionStatus } from '@/utils/sessionStatus'
import { logger } from '@/lib/logging'
interface SessionTableProps {
sessions: Session[]
@@ -243,7 +244,7 @@ export default function SessionTable({
const currentSession = sessions.find(s => s.id === focusedSession?.id)
if (!currentSession) return
console.log('Archive hotkey pressed:', {
logger.log('Archive hotkey pressed:', {
sessionId: currentSession.id,
archived: currentSession.archived,
willArchive: !currentSession.archived,

View File

@@ -2,6 +2,7 @@ import { useState, useEffect, useCallback } from 'react'
import { daemonClient } from '@/lib/daemon'
import { Approval } from '@/lib/daemon/types'
import { formatError } from '@/utils/errors'
import { logger } from '@/lib/logging'
interface UseApprovalsReturn {
approvals: Approval[]
@@ -109,7 +110,7 @@ export function useApprovalsWithSubscription(sessionId?: string): UseApprovalsRe
})
unsubscribe = handle.unsubscribe
} catch (err) {
console.error('Failed to subscribe to events:', err)
logger.error('Failed to subscribe to events:', err)
// Fall back to polling on subscription failure
const interval = setInterval(() => {
if (isSubscribed) {

View File

@@ -1,6 +1,7 @@
import { useState, useEffect, useCallback } from 'react'
import { useState, useEffect, useCallback, useRef } from 'react'
import { daemonClient } from '@/lib/daemon'
import { formatError } from '@/utils/errors'
import { daemonService } from '@/services/daemon-service'
interface UseDaemonConnectionReturn {
connected: boolean
@@ -8,6 +9,7 @@ interface UseDaemonConnectionReturn {
error: string | null
version: string | null
connect: () => Promise<void>
reconnect: () => Promise<void>
checkHealth: () => Promise<void>
}
@@ -16,6 +18,7 @@ export function useDaemonConnection(): UseDaemonConnectionReturn {
const [connecting, setConnecting] = useState(false)
const [error, setError] = useState<string | null>(null)
const [version, setVersion] = useState<string | null>(null)
const retryCount = useRef(0)
const checkHealth = useCallback(async () => {
try {
@@ -31,11 +34,47 @@ export function useDaemonConnection(): UseDaemonConnectionReturn {
}, [])
const connect = useCallback(async () => {
if (connecting) return
setConnecting(true)
setError(null)
try {
await daemonClient.connect()
const health = await daemonClient.health()
setConnected(true)
setVersion(health.version)
retryCount.current = 0
} catch (err: any) {
setConnected(false)
// Check if this is first failure and we have a managed daemon
if (retryCount.current === 0) {
const isManaged = await daemonService.isDaemonRunning()
if (!isManaged) {
// Let DaemonManager handle it
setError(formatError(err))
} else {
// Managed daemon might be starting, retry
retryCount.current++
setTimeout(() => connect(), 2000)
}
} else {
setError(formatError(err))
}
} finally {
setConnecting(false)
}
}, [])
const reconnect = useCallback(async () => {
try {
setConnecting(true)
setError(null)
setConnected(false)
await daemonClient.connect()
await daemonClient.reconnect()
await checkHealth()
} catch (err) {
setError(formatError(err))
@@ -64,6 +103,7 @@ export function useDaemonConnection(): UseDaemonConnectionReturn {
error,
version,
connect,
reconnect,
checkHealth,
}
}

View File

@@ -5,6 +5,7 @@ import { useHotkeysContext } from 'react-hotkeys-hook'
import { SessionTableHotkeysScope } from '@/components/internal/SessionTable'
import { exists } from '@tauri-apps/plugin-fs'
import { homeDir } from '@tauri-apps/api/path'
import { logger } from '@/lib/logging'
interface SessionConfig {
query: string
@@ -264,7 +265,7 @@ export function useSessionLauncherHotkeys() {
e.preventDefault()
setGPrefixMode(false)
// TODO: Navigate to approvals view
console.log('Navigate to approvals (Phase 2)')
logger.log('Navigate to approvals (Phase 2)')
return
}

View File

@@ -1,5 +1,6 @@
import { useEffect, useRef } from 'react'
import { useHotkeysContext } from 'react-hotkeys-hook'
import { logger } from '@/lib/logging'
export function useStealHotkeyScope(targetScope: string) {
const { activeScopes, enableScope, disableScope } = useHotkeysContext()
@@ -7,16 +8,16 @@ export function useStealHotkeyScope(targetScope: string) {
useEffect(() => {
initialScopesRef.current = activeScopes
console.log('disabling scopes (storing initial)', activeScopes)
logger.log('disabling scopes (storing initial)', activeScopes)
activeScopes.forEach(scope => disableScope(scope))
console.log('activating target scope', targetScope)
logger.log('activating target scope', targetScope)
enableScope(targetScope)
return () => {
console.log('disabling target scope', targetScope)
logger.log('disabling target scope', targetScope)
disableScope(targetScope)
console.log('enabling scopes', initialScopesRef.current)
logger.log('enabling scopes', initialScopesRef.current)
initialScopesRef.current.forEach(scope => enableScope(scope))
}
}, [])

View File

@@ -1,5 +1,6 @@
import { useEffect, useRef } from 'react'
import { daemonClient } from '@/lib/daemon'
import { logger } from '@/lib/logging'
import type {
Event,
SessionStatusChangedEventData,
@@ -26,18 +27,18 @@ export function useSessionSubscriptions(
}, [handlers])
useEffect(() => {
console.log('useSubscriptions effect running', {
logger.log('useSubscriptions effect running', {
connected,
isSubscribed: isSubscribedRef.current,
})
if (!connected) {
console.log('Not connected, skipping subscription')
logger.log('Not connected, skipping subscription')
return
}
if (isSubscribedRef.current) {
console.log('Already subscribed, skipping')
logger.log('Already subscribed, skipping')
return
}
@@ -50,12 +51,12 @@ export function useSessionSubscriptions(
// Double-check we're still active after the delay
if (!isActive) {
console.log('Component unmounted during subscription setup')
logger.log('Component unmounted during subscription setup')
return
}
try {
console.log('Creating new subscription...')
logger.log('Creating new subscription...')
// Mark as subscribed immediately to prevent duplicate subscriptions
isSubscribedRef.current = true
@@ -91,9 +92,9 @@ export function useSessionSubscriptions(
})
unsubscribe = subscription.unsubscribe
console.log('Subscription created successfully')
logger.log('Subscription created successfully')
} catch (err) {
console.error('Failed to subscribe to events:', err)
logger.error('Failed to subscribe to events:', err)
// Reset on error to allow retry
isSubscribedRef.current = false
// Handler should deal with fallback behavior
@@ -103,17 +104,17 @@ export function useSessionSubscriptions(
subscribe()
return () => {
console.log('Cleanup: Unsubscribing from session events')
logger.log('Cleanup: Unsubscribing from session events')
isActive = false
// Reset the ref immediately to allow re-subscription
isSubscribedRef.current = false
// Call unsubscribe if it exists
if (unsubscribe) {
console.log('Calling unsubscribe function')
logger.log('Calling unsubscribe function')
unsubscribe()
} else {
console.log('No unsubscribe function available')
logger.log('No unsubscribe function available')
}
}
}, [connected]) // Only re-subscribe when connection status changes

View File

@@ -7,6 +7,7 @@ import {
ConversationEvent,
} from '@humanlayer/hld-sdk'
import { getDaemonUrl, getDefaultHeaders } from './http-config'
import { logger } from '@/lib/logging'
import type {
DaemonClient as IDaemonClient,
LaunchSessionParams,
@@ -61,7 +62,8 @@ export class HTTPDaemonClient implements IDaemonClient {
}
private async doConnect(): Promise<void> {
const baseUrl = getDaemonUrl()
// getDaemonUrl now checks for managed daemon port dynamically
const baseUrl = await getDaemonUrl()
this.client = new HLDClient({
baseUrl: `${baseUrl}/api/v1`,
@@ -82,6 +84,16 @@ export class HTTPDaemonClient implements IDaemonClient {
}
}
async reconnect(): Promise<void> {
// Disconnect first if connected
if (this.connected || this.connectionPromise) {
await this.disconnect()
}
// Now connect to the potentially new URL
return this.connect()
}
async disconnect(): Promise<void> {
// Unsubscribe all event streams
for (const unsubscribe of this.subscriptions.values()) {
@@ -91,6 +103,8 @@ export class HTTPDaemonClient implements IDaemonClient {
this.connected = false
this.client = undefined
this.connectionPromise = undefined
this.retryCount = 0
}
async health(): Promise<HealthCheckResponse> {
@@ -346,10 +360,10 @@ export class HTTPDaemonClient implements IDaemonClient {
})
},
onError: error => {
console.error('Event subscription error:', error)
logger.error('Event subscription error:', error)
// Attempt reconnection
if (!this.connected) {
this.connect().catch(console.error)
this.connect().catch(logger.error)
}
},
onDisconnect: () => {
@@ -362,7 +376,7 @@ export class HTTPDaemonClient implements IDaemonClient {
this.subscriptions.set(subscriptionId, unsubscribe)
})
.catch(error => {
console.error('Failed to start event subscription:', error)
logger.error('Failed to start event subscription:', error)
})
// Return handle for unsubscribing

View File

@@ -1,10 +1,28 @@
// Get daemon URL from environment or default
export function getDaemonUrl(): string {
// Check for explicit URL first
import { daemonService } from '@/services/daemon-service'
import { logger } from '@/lib/logging'
// Get daemon URL from environment or managed daemon
export async function getDaemonUrl(): Promise<string> {
// Check for custom URL from debug panel first
if ((window as any).__HUMANLAYER_DAEMON_URL) {
return (window as any).__HUMANLAYER_DAEMON_URL
}
// Check for explicit URL from environment
if (import.meta.env.VITE_HUMANLAYER_DAEMON_URL) {
return import.meta.env.VITE_HUMANLAYER_DAEMON_URL
}
// Check if we have a managed daemon
try {
const daemonInfo = await daemonService.getDaemonInfo()
if (daemonInfo && daemonInfo.port) {
return `http://localhost:${daemonInfo.port}`
}
} catch (error) {
logger.warn('Failed to get managed daemon info:', error)
}
// Check for port override
const port = import.meta.env.VITE_HUMANLAYER_DAEMON_HTTP_PORT || '7777'
const host = import.meta.env.VITE_HUMANLAYER_DAEMON_HTTP_HOST || 'localhost'
@@ -15,7 +33,7 @@ export function getDaemonUrl(): string {
// Headers to include with all requests
export function getDefaultHeaders(): Record<string, string> {
return {
'X-Client': 'wui',
'X-Client': 'codelayer',
'X-Client-Version': import.meta.env.VITE_APP_VERSION || 'unknown',
}
}

View File

@@ -48,6 +48,7 @@ export interface SubscriptionHandle {
// Client interface using legacy types for backward compatibility
export interface DaemonClient {
connect(): Promise<void>
reconnect(): Promise<void>
disconnect(): Promise<void>
health(): Promise<HealthCheckResponse>

View File

@@ -0,0 +1,50 @@
import { appLogDir } from '@tauri-apps/api/path'
import { invoke } from '@tauri-apps/api/core'
import { openUrl } from '@tauri-apps/plugin-opener'
import { logger } from './logging'
export async function notifyLogLocation() {
try {
let logDir: string
// Try to get custom log directory first (dev mode)
try {
logDir = await invoke<string>('get_log_directory')
} catch {
// Fall back to default app log directory (production)
logDir = await appLogDir()
}
// Log to console (visible in dev mode)
console.log(
`%c📁 Application logs are stored in: ${logDir}`,
'color: #4CAF50; font-weight: bold; font-size: 14px;',
)
// Also log through our logging service
logger.log(`Application logs are stored in: ${logDir}`)
return logDir
} catch (error) {
logger.error('Failed to get log directory:', error)
return null
}
}
export async function openLogDirectory() {
try {
let logDir: string
// Try to get custom log directory first (dev mode)
try {
logDir = await invoke<string>('get_log_directory')
} catch {
// Fall back to default app log directory (production)
logDir = await appLogDir()
}
await openUrl(logDir)
} catch (error) {
logger.error('Failed to open log directory:', error)
}
}

View File

@@ -0,0 +1,107 @@
// Detect if we're in a test environment (no window object)
const isTestEnvironment = typeof window === 'undefined'
// Lazy load Tauri logging to avoid import errors in tests
let tauriLog: any = null
const getTauriLog = async () => {
if (!tauriLog && !isTestEnvironment) {
tauriLog = await import('@tauri-apps/plugin-log')
}
return tauriLog
}
// Create a logging service that preserves console functionality
// while also sending to Tauri's log plugin
export const logger = {
log: (message: string, ...args: any[]) => {
const fullMessage = [message, ...args]
.map(arg => (typeof arg === 'object' ? JSON.stringify(arg) : String(arg)))
.join(' ')
// In test environment, preserve original arguments
if (isTestEnvironment) {
console.log(message, ...args)
return
}
getTauriLog().then(log => log?.info?.(`[Console] ${fullMessage}`))
// Also log to browser console in dev mode
if (import.meta.env.DEV) {
console.log(message, ...args)
}
},
error: (message: string, ...args: any[]) => {
const fullMessage = [message, ...args]
.map(arg => (typeof arg === 'object' ? JSON.stringify(arg) : String(arg)))
.join(' ')
if (isTestEnvironment) {
// In tests, preserve original arguments for test assertions
console.error(message, ...args)
return
}
getTauriLog().then(log => log?.error?.(`[Console] ${fullMessage}`))
if (import.meta.env.DEV) {
console.error(message, ...args)
}
},
warn: (message: string, ...args: any[]) => {
const fullMessage = [message, ...args]
.map(arg => (typeof arg === 'object' ? JSON.stringify(arg) : String(arg)))
.join(' ')
if (isTestEnvironment) {
console.warn(message, ...args)
return
}
getTauriLog().then(log => log?.warn?.(`[Console] ${fullMessage}`))
if (import.meta.env.DEV) {
console.warn(message, ...args)
}
},
debug: (message: string, ...args: any[]) => {
const fullMessage = [message, ...args]
.map(arg => (typeof arg === 'object' ? JSON.stringify(arg) : String(arg)))
.join(' ')
if (isTestEnvironment) {
console.debug(message, ...args)
return
}
getTauriLog().then(log => log?.debug?.(`[Console] ${fullMessage}`))
if (import.meta.env.DEV) {
console.debug(message, ...args)
}
},
trace: (message: string, ...args: any[]) => {
const fullMessage = [message, ...args]
.map(arg => (typeof arg === 'object' ? JSON.stringify(arg) : String(arg)))
.join(' ')
if (isTestEnvironment) {
console.trace(message, ...args)
return
}
getTauriLog().then(log => log?.trace?.(`[Console] ${fullMessage}`))
if (import.meta.env.DEV) {
console.trace(message, ...args)
}
},
}
// Export original console methods for migration
export const browserConsole = {
log: console.log.bind(console),
error: console.error.bind(console),
warn: console.warn.bind(console),
debug: console.debug.bind(console),
trace: console.trace.bind(console),
}

View File

@@ -4,6 +4,11 @@ import { RouterProvider } from 'react-router-dom'
import { router } from './router'
import { ThemeProvider } from './contexts/ThemeContext'
import { HotkeysProvider } from 'react-hotkeys-hook'
import { attachConsole } from '@tauri-apps/plugin-log'
// Initialize console logging bridge to display Tauri logs in browser console
// Note: This does NOT capture frontend console.* calls - it only shows Rust logs in the browser
attachConsole().catch(console.error)
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>

View File

@@ -3,6 +3,7 @@ import { create, StoreApi, useStore } from 'zustand'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { logger } from '@/lib/logging'
// Define the store interface
interface CounterStore {
@@ -58,7 +59,7 @@ class DemoAnimator {
// Subscribe to store changes for logging/debugging
this.unsubscribe = store.subscribe(state => {
console.log('[Demo Store] Count updated to:', state.count)
logger.log('[Demo Store] Count updated to:', state.count)
})
}
@@ -121,7 +122,7 @@ function RealStoreProvider({ children }: { children: React.ReactNode }) {
useEffect(() => {
// Optional: Subscribe to real store for logging
const unsubscribe = realStore.subscribe(state => {
console.log('[Real Store] Count updated to:', state.count)
logger.log('[Real Store] Count updated to:', state.count)
})
return () => {

View File

@@ -9,6 +9,7 @@ import { Session } from '@/lib/daemon/types'
import { ThemeSelector } from '@/components/ThemeSelector'
import { SessionLauncher } from '@/components/SessionLauncher'
import { DemoStoreProvider, useDemoStore } from '@/stores/demo/providers/DemoStoreProvider'
import { logger } from '@/lib/logging'
import {
launcherWorkflowSequence,
statusChangesSequence,
@@ -32,7 +33,7 @@ function SessionTableWrapper() {
})
const handleActivateSession = (session: Session) => {
console.log('Activating session:', session.id)
logger.log('Activating session:', session.id)
// In demo mode, this would navigate to session detail
}

View File

@@ -6,6 +6,7 @@ import {
} from '@tauri-apps/plugin-notification'
import { getCurrentWindow } from '@tauri-apps/api/window'
import { formatError } from '@/utils/errors'
import { logger } from '@/lib/logging'
// Types for generic notification system
export type NotificationType =
@@ -44,7 +45,7 @@ class NotificationService {
private unlistenBlur: (() => void) | null = null
constructor() {
console.log('NotificationService: Constructor called')
logger.log('NotificationService: Constructor called')
this.attachFocusListeners()
}
@@ -56,7 +57,7 @@ class NotificationService {
}
private async attachFocusListeners() {
console.log('NotificationService: Attaching focus listeners')
logger.log('NotificationService: Attaching focus listeners')
// Check initial focus state synchronously
this.validateFocusState()
@@ -70,36 +71,36 @@ class NotificationService {
// Listen for focus events
this.unlistenFocus = await appWindow.onFocusChanged(event => {
console.log('Tauri window focus changed:', event)
logger.log('Tauri window focus changed:', event)
this.appFocused = event.payload
})
// Also use standard window events as fallback
this.focusHandler = () => {
console.log('window focused (standard event)')
logger.log('window focused (standard event)')
this.appFocused = true
}
this.blurHandler = () => {
console.log('window blurred (standard event)')
logger.log('window blurred (standard event)')
this.appFocused = false
}
window.addEventListener('focus', this.focusHandler)
window.addEventListener('blur', this.blurHandler)
console.log('NotificationService: Focus listeners attached')
logger.log('NotificationService: Focus listeners attached')
} catch (error) {
console.error('Failed to attach Tauri focus listeners, using standard events only:', error)
logger.error('Failed to attach Tauri focus listeners, using standard events only:', error)
// Fallback to standard events only
this.focusHandler = () => {
console.log('window focused')
logger.log('window focused')
this.appFocused = true
}
this.blurHandler = () => {
console.log('window blurred')
logger.log('window blurred')
this.appFocused = false
}
@@ -109,7 +110,7 @@ class NotificationService {
}
private async detachFocusListeners() {
console.log('NotificationService: Detaching focus listeners', {
logger.log('NotificationService: Detaching focus listeners', {
hasFocusHandler: !!this.focusHandler,
hasBlurHandler: !!this.blurHandler,
hasUnlistenFocus: !!this.unlistenFocus,
@@ -142,7 +143,7 @@ class NotificationService {
* Clean up resources (useful for hot module replacement)
*/
cleanup() {
console.log('NotificationService: Cleanup called')
logger.log('NotificationService: Cleanup called')
this.detachFocusListeners()
}
@@ -195,7 +196,7 @@ class NotificationService {
? this.isViewingSession(options.metadata.sessionId)
: false
console.log('NotificationService.notify:', {
logger.log('NotificationService.notify:', {
appFocused: this.appFocused,
notificationType: options.type,
sessionId: options.metadata.sessionId,
@@ -208,7 +209,7 @@ class NotificationService {
}
// If app is focused but user is viewing the session, skip in-app notification
else if (isViewingSession) {
console.log(`Skipping in-app notification: User is viewing session ${options.metadata.sessionId}`)
logger.log(`Skipping in-app notification: User is viewing session ${options.metadata.sessionId}`)
return null
}
// Otherwise show in-app notification
@@ -258,11 +259,11 @@ class NotificationService {
* Show OS-level notification using Tauri plugin
*/
private async showOSNotification(options: NotificationOptions) {
console.log('NotificationService.showOSNotification called:', options.title)
logger.log('NotificationService.showOSNotification called:', options.title)
try {
// Check if we have permission
let permissionGranted = await isPermissionGranted()
console.log('OS notification permission granted:', permissionGranted)
logger.log('OS notification permission granted:', permissionGranted)
// Request permission if not granted
if (!permissionGranted) {
@@ -271,7 +272,7 @@ class NotificationService {
}
if (!permissionGranted) {
console.warn('Notification permission not granted, falling back to in-app notification')
logger.warn('Notification permission not granted, falling back to in-app notification')
this.showInAppNotification(options)
return
}
@@ -284,7 +285,7 @@ class NotificationService {
// but at least the notification will bring attention to the app
})
} catch (error) {
console.error('Failed to show OS notification:', error)
logger.error('Failed to show OS notification:', error)
// Fallback to in-app notification
this.showInAppNotification(options)
}
@@ -295,7 +296,7 @@ class NotificationService {
*/
async notifyError(error: unknown, context?: string): Promise<string | null> {
// Always log to console for debugging
console.error(context || 'Error:', error)
logger.error(context || 'Error:', error)
// Format the error message
const formattedMessage = formatError(error)
@@ -372,19 +373,19 @@ let notificationService: NotificationService
// Clean up previous instance on hot reload
if (import.meta.hot) {
console.log('NotificationService: HMR detected, checking for previous instance')
logger.log('NotificationService: HMR detected, checking for previous instance')
if ((import.meta as any).hot.data.notificationService) {
console.log('NotificationService: Cleaning up previous instance')
logger.log('NotificationService: Cleaning up previous instance')
;(import.meta as any).hot.data.notificationService.cleanup()
}
}
console.log('NotificationService: Creating new instance')
logger.log('NotificationService: Creating new instance')
notificationService = new NotificationService()
// Store instance for cleanup on next hot reload
if (import.meta.hot) {
console.log('NotificationService: Storing instance for future cleanup')
logger.log('NotificationService: Storing instance for future cleanup')
;(import.meta as any).hot.data.notificationService = notificationService
}

View File

@@ -0,0 +1,105 @@
import { invoke } from '@tauri-apps/api/core'
import { logger } from '@/lib/logging'
export interface DaemonInfo {
port: number
pid: number
database_path: string
socket_path: string
branch_id: string
}
export interface DaemonConfig {
branch_id: string
port: number
database_path: string
socket_path: string
last_used: string
}
class DaemonService {
private static instance: DaemonService
static getInstance(): DaemonService {
if (!DaemonService.instance) {
DaemonService.instance = new DaemonService()
}
return DaemonService.instance
}
async startDaemon(isDev: boolean = false, branchOverride?: string): Promise<DaemonInfo> {
try {
return await invoke<DaemonInfo>('start_daemon', { isDev, branchOverride })
} catch (error) {
throw new Error(`Failed to start daemon: ${error}`)
}
}
async stopDaemon(): Promise<void> {
try {
await invoke('stop_daemon')
} catch (error) {
throw new Error(`Failed to stop daemon: ${error}`)
}
}
async getDaemonInfo(isDev: boolean = import.meta.env.DEV): Promise<DaemonInfo | null> {
try {
return await invoke<DaemonInfo | null>('get_daemon_info', { isDev })
} catch (error) {
logger.error('Failed to get daemon info:', error)
return null
}
}
async isDaemonRunning(): Promise<boolean> {
try {
return await invoke<boolean>('is_daemon_running')
} catch (error) {
logger.error('Failed to check daemon status:', error)
return false
}
}
async getStoredConfigs(): Promise<DaemonConfig[]> {
try {
return await invoke<DaemonConfig[]>('get_stored_configs')
} catch (error) {
logger.error('Failed to get stored configs:', error)
return []
}
}
async getConfigPath(): Promise<string> {
try {
return await invoke<string>('get_config_path')
} catch {
return 'unknown'
}
}
async connectToExisting(url: string): Promise<void> {
// Validate URL format
try {
new URL(url)
} catch {
throw new Error(`Invalid URL format: ${url}`)
}
// Store the custom URL temporarily (not in Tauri store)
;(window as any).__HUMANLAYER_DAEMON_URL = url
;(window as any).__HUMANLAYER_DAEMON_TYPE = 'external'
}
async switchToManagedDaemon(): Promise<void> {
// Clear the custom URL to switch back to managed daemon
delete (window as any).__HUMANLAYER_DAEMON_URL
delete (window as any).__HUMANLAYER_DAEMON_TYPE
}
getDaemonType(): 'managed' | 'external' {
return (window as any).__HUMANLAYER_DAEMON_TYPE || 'managed'
}
}
export const daemonService = DaemonService.getInstance()

View File

@@ -2,6 +2,7 @@ import type { Session, Approval } from '@/lib/daemon/types'
import { SessionStatus } from '@/lib/daemon/types'
import { create, StoreApi } from 'zustand'
import { daemonClient } from '@/lib/daemon'
import { logger } from '@/lib/logging'
export interface AppState {
/* Sessions */
@@ -110,7 +111,7 @@ export function createRealAppStore(): StoreApi<AppState> {
const response = await daemonClient.getSessionLeaves()
set({ sessions: response.sessions })
} catch (error) {
console.error('Failed to refresh sessions:', error)
logger.error('Failed to refresh sessions:', error)
}
},
setFocusedSession: (session: Session | null) => set({ focusedSession: session }),
@@ -149,7 +150,7 @@ export function createRealAppStore(): StoreApi<AppState> {
await daemonClient.interruptSession(sessionId)
// The session status will be updated via the subscription
} catch (error) {
console.error('Failed to interrupt session:', error)
logger.error('Failed to interrupt session:', error)
}
},
@@ -193,7 +194,7 @@ export function createRealAppStore(): StoreApi<AppState> {
activeSessionId: sessionId,
})
} catch (error) {
console.error('Failed to fetch session detail:', error)
logger.error('Failed to fetch session detail:', error)
throw error
}
},

View File

@@ -4,6 +4,7 @@ import { SessionSlice, createSessionSlice } from './slices/sessionSlice'
import { LauncherSlice, createLauncherSlice } from './slices/launcherSlice'
import { ThemeSlice, createThemeSlice } from './slices/themeSlice'
import { AppSlice, createAppSlice } from './slices/appSlice'
import { logger } from '@/lib/logging'
// Combined store type that includes all slices
export type ComposedDemoStore = SessionSlice & LauncherSlice & ThemeSlice & AppSlice
@@ -97,7 +98,7 @@ export class ComposedDemoAnimator {
const step = this.sequence[this.currentIndex]
this.timeoutId = setTimeout(() => {
console.log(
logger.log(
`[Demo Animator] Step ${this.currentIndex + 1}/${this.sequence.length}: ${step.description || 'State update'}`,
)

View File

@@ -1,6 +1,7 @@
import { create, StoreApi } from 'zustand'
import { Session, SessionStatus } from '@/lib/daemon/types'
import { Theme } from '@/contexts/ThemeContext'
import { logger } from '@/lib/logging'
// Comprehensive app state for demo animations
interface DemoAppState {
@@ -249,7 +250,7 @@ export class DemoAppAnimator {
// Subscribe to store changes for logging
this.unsubscribe = store.subscribe(state => {
console.log('[Demo App Store] State updated:', {
logger.log('[Demo App Store] State updated:', {
sessions: state.sessions.length,
launcher: state.launcherOpen,
theme: state.theme,
@@ -287,7 +288,7 @@ export class DemoAppAnimator {
const step = this.sequence[this.currentIndex]
this.timeoutId = setTimeout(() => {
console.log(
logger.log(
`[Demo App Animator] Step ${this.currentIndex + 1}/${this.sequence.length}: ${step.description || 'State update'}`,
)

View File

@@ -1,5 +1,6 @@
import React, { createContext, useContext, useState, useEffect } from 'react'
import { StoreApi, useStore } from 'zustand'
import { logger } from '@/lib/logging'
import {
ComposedDemoStore,
createComposedDemoStore,
@@ -61,8 +62,8 @@ export function useDemoAnimator() {
// In a real implementation, we'd expose the animator instance
// For now, return basic controls
return {
pause: () => console.log('Pause not implemented'),
resume: () => console.log('Resume not implemented'),
reset: () => console.log('Reset not implemented'),
pause: () => logger.log('Pause not implemented'),
resume: () => logger.log('Resume not implemented'),
reset: () => logger.log('Reset not implemented'),
}
}

View File

@@ -1,5 +1,6 @@
import { toast } from 'sonner'
import { writeText } from '@tauri-apps/plugin-clipboard-manager'
import { logger } from '@/lib/logging'
export async function copyToClipboard(text: string): Promise<boolean> {
try {
@@ -7,7 +8,7 @@ export async function copyToClipboard(text: string): Promise<boolean> {
toast.success('Copied to clipboard')
return true
} catch (error) {
console.error('Failed to copy to clipboard:', error instanceof Error ? error.message : error)
logger.error('Failed to copy to clipboard:', error instanceof Error ? error.message : error)
toast.error('Failed to copy to clipboard')
return false
}