
Concierge
- 23 installs
- Updated February 10, 2026
- skillhq/concierge
Helps with ai & agent building tasks.
About
concierge is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- concierge
- AI & Agent Building
- AI-coding skill
Concierge by the numbers
- 23 all-time installs (skills.sh)
- Ranked #10,032 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/skillhq/concierge --skill conciergeAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 23 |
|---|---|
| Last updated | February 10, 2026 |
| Repository | skillhq/concierge ↗ |
What it does
Helps with ai & agent building tasks.
Files
Concierge
Find contact details (phone, email, WhatsApp, Instagram, etc.) for listings and businesses, search for places via Google Places, check availability on Booking.com, and place autonomous AI phone calls.
Capabilities
1) Find contacts or search for places
The unified find command auto-detects whether you pass a listing URL or a text query.
# URL → contact dossier (same as old find-contact)
concierge find "https://www.airbnb.com/rooms/12345"
# Text query → search Google Places
concierge find "hotels in San Francisco" --limit 5
concierge find "Trisara Resort Phuket"
concierge find "37.7749,-122.4194" --radius-m 5000 --type hotel
# Enrich multi-result with website scraping (slower)
concierge find "hotels in Phuket" --limit 3 --enrichAliases: find-contact, fc, search, s
Options (text search):
-l, --limit <n>- Max results (default: 10, max: 20)--min-rating <n>- Minimum rating (0-5)--type <type>- Place type: lodging, hotel, resort_hotel (default: lodging)--radius-m <meters>- Search radius for coordinate searches--enrich- Scrape websites for email/social (auto-enabled for single results)
Options (URL lookup):
--html <file>- Path to saved HTML file (for offline/pre-fetched content)
Search backend: Prefers goplaces CLI. Falls back to direct Google Places API (1 result max) if configured.
3) Check hotel availability on Booking.com
concierge check-availability "Park Hyatt Tokyo" -i 2024-03-15 -o 2024-03-17
concierge ca "https://www.booking.com/hotel/us/hilton.html" -i 2024-03-15 -o 2024-03-17 --json
concierge availability "Hilton NYC" -i 2024-04-01 -o 2024-04-03 -g 3 --screenshot results.pngOptions:
-i, --check-in <date>- Check-in date (YYYY-MM-DD) required-o, --check-out <date>- Check-out date (YYYY-MM-DD) required-g, --guests <n>- Number of guests (default: 2)-r, --rooms <n>- Number of rooms (default: 1)-s, --screenshot <path>- Save screenshot of results--headed- Show browser window (for debugging)
Requires: agent-browser CLI installed (with Playwright browsers)
4) Place an autonomous phone call
concierge call "+1-555-123-4567" \
--goal "Book a room for March 12-14" \
--name "Derek Rein" \
--email "alexanderderekrein@gmail.com" \
--customer-phone "+1-555-000-1111" \
--context "Prefer direct booking if rate beats Booking.com"The call command now auto-manages infra by default: if local server is down, it starts ngrok + call server automatically and stops both when the call ends.
5) Direct-booking negotiation (minimal inputs)
Uses customer defaults from config and only needs dates plus optional Booking.com price and room type.
concierge direct-booking "+6676310100" \
--hotel "Trisara Resort" \
-i 2026-05-06 -o 2026-05-09 \
--room "Ocean View Pool Junior Suite" \
--booking-price 115000 \
--currency THBIf --room is omitted, the assistant asks for the cheapest available room. If --booking-price is omitted, it negotiates for the best direct rate and value-adds.
Supported listing platforms
- Airbnb:
airbnb.com/rooms/... - Booking.com:
booking.com/hotel/... - VRBO:
vrbo.com/... - Expedia:
expedia.com/...Hotel...
Examples
Find contacts for an Airbnb listing
Run:
concierge find "https://www.airbnb.com/rooms/12345"Search for hotels in a city
Run:
concierge find "hotels in Tokyo" --limit 5 --min-rating 4Search with full enrichment
Run:
concierge find "hotels in Phuket" --limit 3 --enrichCheck availability for a specific hotel
Run:
concierge ca "Hilton Garden Inn Times Square" -i 2024-05-01 -o 2024-05-03Start a call and control turns manually
Run:
concierge call "+1-555-123-4567" \
--goal "Negotiate a direct booking rate" \
--name "Derek Rein" \
--email "alexanderderekrein@gmail.com" \
--customer-phone "+1-555-000-1111" \
--interactiveDirect-booking negotiation with config defaults
Run:
concierge direct-booking "+6676310100" \
--hotel "Trisara Resort" \
-i 2026-05-06 -o 2026-05-09 \
--booking-price 115000 \
--currency THBJSON output for scripting
concierge find --json "https://..."
concierge find --json "hotels in Tokyo" --limit 5Verbose output
concierge --verbose find "https://..."Configuration
The CLI stores configuration in:
~/.config/concierge/config.json5
Optional for contact lookup
concierge config set googlePlacesApiKey "your-key"Required for AI phone calls
concierge config set twilioAccountSid "<sid>"
concierge config set twilioAuthToken "<token>"
concierge config set twilioPhoneNumber "+14155551234"
concierge config set deepgramApiKey "<key>"
concierge config set elevenLabsApiKey "<key>"
concierge config set elevenLabsVoiceId "EXAVITQu4vr4xnSDxMaL"
concierge config set anthropicApiKey "<key>"
# Customer defaults for direct-booking
concierge config set customerName "Derek Rein"
concierge config set customerEmail "derek@example.com"
concierge config set customerPhone "+1-555-000-1111"
# Optional for auto ngrok auth
concierge config set ngrokAuthToken "<token>"Check values:
concierge config showExternal Dependencies
| Feature | Required CLI | Install |
|---|---|---|
find (text search) | goplaces | See goplaces documentation |
check-availability | agent-browser | npm install -g agent-browser && npx playwright install chromium |
call | ffmpeg, ngrok | brew install ffmpeg ngrok |
Notes
- Contact extraction uses publicly available information.
find(text search) uses Google Places API via thegoplacesCLI, with fallback to direct API.check-availabilityuses browser automation to scrape Booking.com. Results depend on DOM structure which may change.callvalidates local dependencies before dialing (ffmpegwith MP3 decode support, andngrokwhen auto-infra is needed).callruns preflight checks for Twilio, Deepgram, and ElevenLabs quota before dialing.- When auto infra is used, server/ngrok logs are written under
~/.config/concierge/call-runs/<run-id>/.
# Only publish SKILL.md and README.md for the skill
# Exclude everything else
# Source code
src/
dist/
tests/
scripts/
# Config files
*.json
*.json5
*.yaml
*.yml
*.toml
biome.json
tsconfig*.json
pnpm-lock.yaml
# Docs (except README and SKILL)
CLAUDE.md
CONTRIBUTING.md
LICENSE
# GitHub
.github/
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm run lint
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm run test
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm run build
name: Release
on:
push:
tags:
- 'v*'
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
registry-url: 'https://registry.npmjs.org'
- run: pnpm install --frozen-lockfile
- run: pnpm run build
- run: pnpm run test
- run: npm publish --provenance --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
# Dependencies
node_modules/
# Build output
dist/
# Environment
.env
.env.local
.env.*.local
# IDE
.idea/
.vscode/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Logs
*.log
npm-debug.log*
# Test coverage
coverage/
# Eval results (generated artifacts)
eval-results/
# Ad-hoc test scripts
test-*.mjs
# Call recordings
calls/
{
"$schema": "https://biomejs.dev/schemas/2.3.13/schema.json",
"assist": { "actions": { "source": { "organizeImports": "on" } } },
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"complexity": {
"noForEach": "off"
},
"suspicious": {
"noExplicitAny": "off",
"noImplicitAnyLet": "off",
"noAssignInExpressions": "off"
},
"style": {
"noNonNullAssertion": "off"
}
}
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 120
},
"javascript": {
"formatter": {
"quoteStyle": "single",
"semicolons": "always",
"trailingCommas": "all"
}
},
"files": {
"includes": ["**", "!**/dist", "!**/node_modules", "!**/*.d.ts"]
}
}
Voice Call Setup Guide
This guide walks you through setting up the voice call system for concierge.
Overview
The voice call system uses:
- Twilio - Phone number and call routing
- Deepgram - Real-time speech-to-text
- ElevenLabs - Text-to-speech with natural voices
- Anthropic Claude - AI conversation intelligence
- ngrok - Local tunnel for Twilio webhooks
Step 1: Create Accounts
Twilio (Phone Calls)
1. Sign up at https://www.twilio.com/try-twilio
- Free trial includes $15.50 credit (enough for ~10 hours of calls)
- You'll need to verify your email and phone number
2. Get your credentials from the Console Dashboard (https://console.twilio.com):
- Account SID - Visible on the dashboard, starts with
AC - Auth Token - Click "Show" to reveal, starts with a lowercase letter
3. Buy a phone number (this is your outbound caller ID):
- Go to: Console → Phone Numbers → Manage → Buy a number
- Or direct link: https://console.twilio.com/us1/develop/phone-numbers/manage/search
- Click "Buy a number" (blue button)
- Search options:
- Country: Select your country (US numbers work best)
- Capabilities: Check "Voice" (required), SMS optional
- Number type: "Local" is cheapest (~$1.15/month)
- Click "Search" to see available numbers
- Pick any number and click "Buy" → "Buy +1XXX..."
- Cost: ~$1.15/month (deducted from your trial credit)
4. Verify the number is ready:
- Go to: Console → Phone Numbers → Manage → Active numbers
- You should see your new number listed
- Copy the full number in E.164 format (e.g.,
+14155551234)
Deepgram (Speech-to-Text)
1. Sign up at https://console.deepgram.com/signup
- Free tier includes $200 credit
2. Create an API key:
- API Keys → Create a New API Key
- Copy the key (only shown once!)
ElevenLabs (Text-to-Speech)
1. Sign up at https://elevenlabs.io/sign-up
- Free tier: 10,000 characters/month
2. Get your API key:
- Profile Settings → API Keys → Create API Key
3. Choose a voice:
- Voice Library → Pick a voice
- Copy the Voice ID from the URL or voice details
- Recommended: "Rachel" (
EXAVITQu4vr4xnSDxMaL) or "Adam" (pNInz6obpgDQGcFmaJgB)
Anthropic (AI Conversation)
1. Sign up at https://console.anthropic.com 2. Get your API key:
- Go to API Keys section
- Create a new key and copy it
ngrok (Local Tunnel)
1. Sign up at https://ngrok.com
- Free tier works fine
2. Install ngrok:
# macOS
brew install ngrok
# Or download from https://ngrok.com/download3. Authenticate:
ngrok config add-authtoken <your-token>Step 2: Configure concierge
Set all required configuration values:
# Twilio credentials (from https://console.twilio.com)
concierge config set twilioAccountSid ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
concierge config set twilioAuthToken xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# Use the full E.164 format with country code, e.g., +14155551234
concierge config set twilioPhoneNumber +14155551234
# Deepgram
concierge config set deepgramApiKey xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# ElevenLabs
concierge config set elevenLabsApiKey xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
concierge config set elevenLabsVoiceId EXAVITQu4vr4xnSDxMaL
# Anthropic (for AI conversation)
concierge config set anthropicApiKey sk-ant-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# Optional: custom port (default: 3000)
concierge config set callServerPort 3000Verify your configuration:
concierge config showStep 3: Start the Server
1. Start ngrok (keep this running):
ngrok http 3000Copy the HTTPS URL (e.g., https://abc123.ngrok.io)
2. Start the call server:
concierge server start --public-url https://abc123.ngrok.io3. Verify it's running:
concierge server statusStep 4: Test a Call
Call your own phone to test:
concierge call "+1-YOUR-PHONE-NUMBER" \
--goal "Test the connection" \
--context "This is a test call"You should: 1. Receive a call from your Twilio number 2. Hear the AI voice when you answer 3. See transcriptions in your terminal
Usage Examples
Book a hotel room
concierge call "+1-800-HILTON" \
--goal "Book a room for 2 nights" \
--context "Check-in: Feb 15, Guest: John Smith, King bed, non-smoking"Make a restaurant reservation
concierge call "+1-555-RESTAURANT" \
--goal "Make a dinner reservation" \
--context "Party of 4, 7:30 PM Saturday, name: Garcia"Interactive mode
For real-time control over what the AI says:
concierge call "+1-555-1234" \
--goal "Custom conversation" \
--interactiveArchitecture
┌─────────────────────────────────────────────────────────────────┐
│ concierge call │
│ "Call the hotel at +1-555-1234 and book a room for Feb 15" │
└──────────────────────────────┬──────────────────────────────────┘
│ HTTP/WebSocket
▼
┌─────────────────────────────────────────────────────────────────┐
│ Call Server (Node.js) │
│ ┌─────────────────────────────────────────────────────────────┐│
│ │ Call Session ││
│ │ ├── Deepgram STT: human speech → text ││
│ │ ├── ConversationAI: text → Claude → AI response ││
│ │ └── ElevenLabs TTS: AI response → speech ││
│ └─────────────────────────────────────────────────────────────┘│
│ - POST /call - Initiate calls │
│ - WS /control - Command channel │
│ - WS /media-stream - Twilio audio stream │
└──────────────────────────────┬──────────────────────────────────┘
│ ngrok tunnel
▼
┌─────────────────────────────────────────────────────────────────┐
│ Twilio Media Streams │
│ - Real-time bidirectional audio │
│ - 8kHz mulaw format │
└─────────────────────────────────────────────────────────────────┘The call flow: 1. Human speaks → Twilio captures audio → Deepgram transcribes 2. Transcript sent to Claude with the call's goal 3. Claude generates contextual response 4. ElevenLabs converts response to speech 5. Audio sent back through Twilio to the caller
Troubleshooting
"Server is not running"
Start the server first:
concierge server start --public-url <your-ngrok-url>"Missing required configuration"
Check which keys are missing:
concierge config showCall doesn't connect
1. Check Twilio console for errors 2. Verify ngrok is running and URL is correct 3. Check Twilio account balance
Poor audio quality
- This is expected - phone audio is 8kHz
- The system uses models optimized for telephony
High latency
Target is <500ms voice-to-voice. If higher: 1. Check internet connection 2. Try a different ElevenLabs voice 3. Ensure server is running locally, not remotely
Costs
Approximate costs per call (varies by region and duration):
| Service | Cost |
|---|---|
| Twilio outbound call | ~$0.02/min |
| Twilio phone number | ~$1.15/month |
| Deepgram | ~$0.0043/min |
| ElevenLabs | ~$0.18/1000 chars |
| Anthropic Claude | ~$0.003/1000 tokens |
A typical 5-minute call costs approximately $0.30-0.50.
Security Notes
- API keys are stored in
~/.config/concierge/config.json5 - Keys are masked when displayed with
config show - The server runs locally - don't expose it directly to the internet
- Use ngrok only for development; consider proper hosting for production
{
"name": "@skillhq/concierge",
"version": "1.18.3",
"description": "Personal AI concierge for contact lookup, availability checks, and autonomous phone calls",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"bin": {
"concierge": "./dist/cli.js"
},
"scripts": {
"build": "tsc",
"dev": "tsc --watch",
"start": "node dist/cli.js",
"clean": "rm -rf dist",
"typecheck": "tsc --noEmit",
"test": "vitest run",
"test:watch": "vitest",
"lint": "biome check .",
"lint:fix": "biome check --write .",
"format": "biome format --write ."
},
"keywords": [
"ai-agent",
"concierge",
"airbnb",
"booking",
"vrbo",
"expedia",
"contact",
"cli"
],
"author": "",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/skillhq/concierge.git"
},
"homepage": "https://github.com/skillhq/concierge#readme",
"bugs": {
"url": "https://github.com/skillhq/concierge/issues"
},
"files": [
"dist"
],
"dependencies": {
"@anthropic-ai/sdk": "^0.72.0",
"@deepgram/sdk": "^3.5.0",
"@elevenlabs/elevenlabs-js": "^2.33.0",
"commander": "^14.0.0",
"json5": "^2.2.3",
"kleur": "^4.1.5",
"twilio": "^5.0.0",
"ws": "^8.16.0"
},
"devDependencies": {
"@biomejs/biome": "^2.3.0",
"@types/node": "^22.0.0",
"@types/ws": "^8.5.10",
"typescript": "^5.4.0",
"vitest": "^4.0.0"
},
"engines": {
"node": ">=18.0.0"
},
"packageManager": "pnpm@10.25.0"
}
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
'@anthropic-ai/sdk':
specifier: ^0.72.0
version: 0.72.0(zod@4.3.6)
'@deepgram/sdk':
specifier: ^3.5.0
version: 3.13.0
'@elevenlabs/elevenlabs-js':
specifier: ^2.33.0
version: 2.33.0
commander:
specifier: ^14.0.0
version: 14.0.2
json5:
specifier: ^2.2.3
version: 2.2.3
kleur:
specifier: ^4.1.5
version: 4.1.5
twilio:
specifier: ^5.0.0
version: 5.12.0
ws:
specifier: ^8.16.0
version: 8.19.0
devDependencies:
'@biomejs/biome':
specifier: ^2.3.0
version: 2.3.13
'@types/node':
specifier: ^22.0.0
version: 22.19.7
'@types/ws':
specifier: ^8.5.10
version: 8.18.1
typescript:
specifier: ^5.4.0
version: 5.9.3
vitest:
specifier: ^4.0.0
version: 4.0.18(@types/node@22.19.7)
packages:
'@anthropic-ai/sdk@0.72.0':
resolution: {integrity: sha512-Wv1NKK6wvAEpcnX9GhEtJQvJjGT6jB4M2Cur/O9YO+2njBmuxerx83M/jo5PQ0ROme0zcisW6b7lImqnnLY7aQ==}
hasBin: true
peerDependencies:
zod: ^3.25.0 || ^4.0.0
peerDependenciesMeta:
zod:
optional: true
'@babel/runtime@7.28.6':
resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==}
engines: {node: '>=6.9.0'}
'@biomejs/biome@2.3.13':
resolution: {integrity: sha512-Fw7UsV0UAtWIBIm0M7g5CRerpu1eKyKAXIazzxhbXYUyMkwNrkX/KLkGI7b+uVDQ5cLUMfOC9vR60q9IDYDstA==}
engines: {node: '>=14.21.3'}
hasBin: true
'@biomejs/cli-darwin-arm64@2.3.13':
resolution: {integrity: sha512-0OCwP0/BoKzyJHnFdaTk/i7hIP9JHH9oJJq6hrSCPmJPo8JWcJhprK4gQlhFzrwdTBAW4Bjt/RmCf3ZZe59gwQ==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [darwin]
'@biomejs/cli-darwin-x64@2.3.13':
resolution: {integrity: sha512-AGr8OoemT/ejynbIu56qeil2+F2WLkIjn2d8jGK1JkchxnMUhYOfnqc9sVzcRxpG9Ycvw4weQ5sprRvtb7Yhcw==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [darwin]
'@biomejs/cli-linux-arm64-musl@2.3.13':
resolution: {integrity: sha512-TUdDCSY+Eo/EHjhJz7P2GnWwfqet+lFxBZzGHldrvULr59AgahamLs/N85SC4+bdF86EhqDuuw9rYLvLFWWlXA==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [linux]
'@biomejs/cli-linux-arm64@2.3.13':
resolution: {integrity: sha512-xvOiFkrDNu607MPMBUQ6huHmBG1PZLOrqhtK6pXJW3GjfVqJg0Z/qpTdhXfcqWdSZHcT+Nct2fOgewZvytESkw==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [linux]
'@biomejs/cli-linux-x64-musl@2.3.13':
resolution: {integrity: sha512-0bdwFVSbbM//Sds6OjtnmQGp4eUjOTt6kHvR/1P0ieR9GcTUAlPNvPC3DiavTqq302W34Ae2T6u5VVNGuQtGlQ==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [linux]
'@biomejs/cli-linux-x64@2.3.13':
resolution: {integrity: sha512-s+YsZlgiXNq8XkgHs6xdvKDFOj/bwTEevqEY6rC2I3cBHbxXYU1LOZstH3Ffw9hE5tE1sqT7U23C00MzkXztMw==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [linux]
'@biomejs/cli-win32-arm64@2.3.13':
resolution: {integrity: sha512-QweDxY89fq0VvrxME+wS/BXKmqMrOTZlN9SqQ79kQSIc3FrEwvW/PvUegQF6XIVaekncDykB5dzPqjbwSKs9DA==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [win32]
'@biomejs/cli-win32-x64@2.3.13':
resolution: {integrity: sha512-trDw2ogdM2lyav9WFQsdsfdVy1dvZALymRpgmWsvSez0BJzBjulhOT/t+wyKeh3pZWvwP3VMs1SoOKwO3wecMQ==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [win32]
'@deepgram/captions@1.2.0':
resolution: {integrity: sha512-8B1C/oTxTxyHlSFubAhNRgCbQ2SQ5wwvtlByn8sDYZvdDtdn/VE2yEPZ4BvUnrKWmsbTQY6/ooLV+9Ka2qmDSQ==}
engines: {node: '>=18.0.0'}
'@deepgram/sdk@3.13.0':
resolution: {integrity: sha512-VFN4gbyIDl5Bj0ApERq4QJHzO2AK4YOoL5Lfoy//q9531+UIzaKCNie4NkBM2Kn/UndHH7tIR335SH14e2r+Ag==}
engines: {node: '>=18.0.0'}
'@elevenlabs/elevenlabs-js@2.33.0':
resolution: {integrity: sha512-cWUkm39kLW+Nbl+NZ51/0GUfqGvSTbzR0zFRq5Asvu7Dm9eFfMVjkRXGezgNo7gzxhX3WbqTAfP9X3BL5vgUqA==}
engines: {node: '>=18.0.0'}
'@esbuild/aix-ppc64@0.27.2':
resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [aix]
'@esbuild/android-arm64@0.27.2':
resolution: {integrity: sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==}
engines: {node: '>=18'}
cpu: [arm64]
os: [android]
'@esbuild/android-arm@0.27.2':
resolution: {integrity: sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==}
engines: {node: '>=18'}
cpu: [arm]
os: [android]
'@esbuild/android-x64@0.27.2':
resolution: {integrity: sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==}
engines: {node: '>=18'}
cpu: [x64]
os: [android]
'@esbuild/darwin-arm64@0.27.2':
resolution: {integrity: sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [darwin]
'@esbuild/darwin-x64@0.27.2':
resolution: {integrity: sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==}
engines: {node: '>=18'}
cpu: [x64]
os: [darwin]
'@esbuild/freebsd-arm64@0.27.2':
resolution: {integrity: sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==}
engines: {node: '>=18'}
cpu: [arm64]
os: [freebsd]
'@esbuild/freebsd-x64@0.27.2':
resolution: {integrity: sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==}
engines: {node: '>=18'}
cpu: [x64]
os: [freebsd]
'@esbuild/linux-arm64@0.27.2':
resolution: {integrity: sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [linux]
'@esbuild/linux-arm@0.27.2':
resolution: {integrity: sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==}
engines: {node: '>=18'}
cpu: [arm]
os: [linux]
'@esbuild/linux-ia32@0.27.2':
resolution: {integrity: sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==}
engines: {node: '>=18'}
cpu: [ia32]
os: [linux]
'@esbuild/linux-loong64@0.27.2':
resolution: {integrity: sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==}
engines: {node: '>=18'}
cpu: [loong64]
os: [linux]
'@esbuild/linux-mips64el@0.27.2':
resolution: {integrity: sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==}
engines: {node: '>=18'}
cpu: [mips64el]
os: [linux]
'@esbuild/linux-ppc64@0.27.2':
resolution: {integrity: sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [linux]
'@esbuild/linux-riscv64@0.27.2':
resolution: {integrity: sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==}
engines: {node: '>=18'}
cpu: [riscv64]
os: [linux]
'@esbuild/linux-s390x@0.27.2':
resolution: {integrity: sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==}
engines: {node: '>=18'}
cpu: [s390x]
os: [linux]
'@esbuild/linux-x64@0.27.2':
resolution: {integrity: sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==}
engines: {node: '>=18'}
cpu: [x64]
os: [linux]
'@esbuild/netbsd-arm64@0.27.2':
resolution: {integrity: sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [netbsd]
'@esbuild/netbsd-x64@0.27.2':
resolution: {integrity: sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==}
engines: {node: '>=18'}
cpu: [x64]
os: [netbsd]
'@esbuild/openbsd-arm64@0.27.2':
resolution: {integrity: sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openbsd]
'@esbuild/openbsd-x64@0.27.2':
resolution: {integrity: sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==}
engines: {node: '>=18'}
cpu: [x64]
os: [openbsd]
'@esbuild/openharmony-arm64@0.27.2':
resolution: {integrity: sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openharmony]
'@esbuild/sunos-x64@0.27.2':
resolution: {integrity: sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==}
engines: {node: '>=18'}
cpu: [x64]
os: [sunos]
'@esbuild/win32-arm64@0.27.2':
resolution: {integrity: sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [win32]
'@esbuild/win32-ia32@0.27.2':
resolution: {integrity: sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==}
engines: {node: '>=18'}
cpu: [ia32]
os: [win32]
'@esbuild/win32-x64@0.27.2':
resolution: {integrity: sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [win32]
'@jridgewell/sourcemap-codec@1.5.5':
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
'@rollup/rollup-android-arm-eabi@4.57.0':
resolution: {integrity: sha512-tPgXB6cDTndIe1ah7u6amCI1T0SsnlOuKgg10Xh3uizJk4e5M1JGaUMk7J4ciuAUcFpbOiNhm2XIjP9ON0dUqA==}
cpu: [arm]
os: [android]
'@rollup/rollup-android-arm64@4.57.0':
resolution: {integrity: sha512-sa4LyseLLXr1onr97StkU1Nb7fWcg6niokTwEVNOO7awaKaoRObQ54+V/hrF/BP1noMEaaAW6Fg2d/CfLiq3Mg==}
cpu: [arm64]
os: [android]
'@rollup/rollup-darwin-arm64@4.57.0':
resolution: {integrity: sha512-/NNIj9A7yLjKdmkx5dC2XQ9DmjIECpGpwHoGmA5E1AhU0fuICSqSWScPhN1yLCkEdkCwJIDu2xIeLPs60MNIVg==}
cpu: [arm64]
os: [darwin]
'@rollup/rollup-darwin-x64@4.57.0':
resolution: {integrity: sha512-xoh8abqgPrPYPr7pTYipqnUi1V3em56JzE/HgDgitTqZBZ3yKCWI+7KUkceM6tNweyUKYru1UMi7FC060RyKwA==}
cpu: [x64]
os: [darwin]
'@rollup/rollup-freebsd-arm64@4.57.0':
resolution: {integrity: sha512-PCkMh7fNahWSbA0OTUQ2OpYHpjZZr0hPr8lId8twD7a7SeWrvT3xJVyza+dQwXSSq4yEQTMoXgNOfMCsn8584g==}
cpu: [arm64]
os: [freebsd]
'@rollup/rollup-freebsd-x64@4.57.0':
resolution: {integrity: sha512-1j3stGx+qbhXql4OCDZhnK7b01s6rBKNybfsX+TNrEe9JNq4DLi1yGiR1xW+nL+FNVvI4D02PUnl6gJ/2y6WJA==}
cpu: [x64]
os: [freebsd]
'@rollup/rollup-linux-arm-gnueabihf@4.57.0':
resolution: {integrity: sha512-eyrr5W08Ms9uM0mLcKfM/Uzx7hjhz2bcjv8P2uynfj0yU8GGPdz8iYrBPhiLOZqahoAMB8ZiolRZPbbU2MAi6Q==}
cpu: [arm]
os: [linux]
'@rollup/rollup-linux-arm-musleabihf@4.57.0':
resolution: {integrity: sha512-Xds90ITXJCNyX9pDhqf85MKWUI4lqjiPAipJ8OLp8xqI2Ehk+TCVhF9rvOoN8xTbcafow3QOThkNnrM33uCFQA==}
cpu: [arm]
os: [linux]
'@rollup/rollup-linux-arm64-gnu@4.57.0':
resolution: {integrity: sha512-Xws2KA4CLvZmXjy46SQaXSejuKPhwVdaNinldoYfqruZBaJHqVo6hnRa8SDo9z7PBW5x84SH64+izmldCgbezw==}
cpu: [arm64]
os: [linux]
'@rollup/rollup-linux-arm64-musl@4.57.0':
resolution: {integrity: sha512-hrKXKbX5FdaRJj7lTMusmvKbhMJSGWJ+w++4KmjiDhpTgNlhYobMvKfDoIWecy4O60K6yA4SnztGuNTQF+Lplw==}
cpu: [arm64]
os: [linux]
'@rollup/rollup-linux-loong64-gnu@4.57.0':
resolution: {integrity: sha512-6A+nccfSDGKsPm00d3xKcrsBcbqzCTAukjwWK6rbuAnB2bHaL3r9720HBVZ/no7+FhZLz/U3GwwZZEh6tOSI8Q==}
cpu: [loong64]
os: [linux]
'@rollup/rollup-linux-loong64-musl@4.57.0':
resolution: {integrity: sha512-4P1VyYUe6XAJtQH1Hh99THxr0GKMMwIXsRNOceLrJnaHTDgk1FTcTimDgneRJPvB3LqDQxUmroBclQ1S0cIJwQ==}
cpu: [loong64]
os: [linux]
'@rollup/rollup-linux-ppc64-gnu@4.57.0':
resolution: {integrity: sha512-8Vv6pLuIZCMcgXre6c3nOPhE0gjz1+nZP6T+hwWjr7sVH8k0jRkH+XnfjjOTglyMBdSKBPPz54/y1gToSKwrSQ==}
cpu: [ppc64]
os: [linux]
'@rollup/rollup-linux-ppc64-musl@4.57.0':
resolution: {integrity: sha512-r1te1M0Sm2TBVD/RxBPC6RZVwNqUTwJTA7w+C/IW5v9Ssu6xmxWEi+iJQlpBhtUiT1raJ5b48pI8tBvEjEFnFA==}
cpu: [ppc64]
os: [linux]
'@rollup/rollup-linux-riscv64-gnu@4.57.0':
resolution: {integrity: sha512-say0uMU/RaPm3CDQLxUUTF2oNWL8ysvHkAjcCzV2znxBr23kFfaxocS9qJm+NdkRhF8wtdEEAJuYcLPhSPbjuQ==}
cpu: [riscv64]
os: [linux]
'@rollup/rollup-linux-riscv64-musl@4.57.0':
resolution: {integrity: sha512-/MU7/HizQGsnBREtRpcSbSV1zfkoxSTR7wLsRmBPQ8FwUj5sykrP1MyJTvsxP5KBq9SyE6kH8UQQQwa0ASeoQQ==}
cpu: [riscv64]
os: [linux]
'@rollup/rollup-linux-s390x-gnu@4.57.0':
resolution: {integrity: sha512-Q9eh+gUGILIHEaJf66aF6a414jQbDnn29zeu0eX3dHMuysnhTvsUvZTCAyZ6tJhUjnvzBKE4FtuaYxutxRZpOg==}
cpu: [s390x]
os: [linux]
'@rollup/rollup-linux-x64-gnu@4.57.0':
resolution: {integrity: sha512-OR5p5yG5OKSxHReWmwvM0P+VTPMwoBS45PXTMYaskKQqybkS3Kmugq1W+YbNWArF8/s7jQScgzXUhArzEQ7x0A==}
cpu: [x64]
os: [linux]
'@rollup/rollup-linux-x64-musl@4.57.0':
resolution: {integrity: sha512-XeatKzo4lHDsVEbm1XDHZlhYZZSQYym6dg2X/Ko0kSFgio+KXLsxwJQprnR48GvdIKDOpqWqssC3iBCjoMcMpw==}
cpu: [x64]
os: [linux]
'@rollup/rollup-openbsd-x64@4.57.0':
resolution: {integrity: sha512-Lu71y78F5qOfYmubYLHPcJm74GZLU6UJ4THkf/a1K7Tz2ycwC2VUbsqbJAXaR6Bx70SRdlVrt2+n5l7F0agTUw==}
cpu: [x64]
os: [openbsd]
'@rollup/rollup-openharmony-arm64@4.57.0':
resolution: {integrity: sha512-v5xwKDWcu7qhAEcsUubiav7r+48Uk/ENWdr82MBZZRIm7zThSxCIVDfb3ZeRRq9yqk+oIzMdDo6fCcA5DHfMyA==}
cpu: [arm64]
os: [openharmony]
'@rollup/rollup-win32-arm64-msvc@4.57.0':
resolution: {integrity: sha512-XnaaaSMGSI6Wk8F4KK3QP7GfuuhjGchElsVerCplUuxRIzdvZ7hRBpLR0omCmw+kI2RFJB80nenhOoGXlJ5TfQ==}
cpu: [arm64]
os: [win32]
'@rollup/rollup-win32-ia32-msvc@4.57.0':
resolution: {integrity: sha512-3K1lP+3BXY4t4VihLw5MEg6IZD3ojSYzqzBG571W3kNQe4G4CcFpSUQVgurYgib5d+YaCjeFow8QivWp8vuSvA==}
cpu: [ia32]
os: [win32]
'@rollup/rollup-win32-x64-gnu@4.57.0':
resolution: {integrity: sha512-MDk610P/vJGc5L5ImE4k5s+GZT3en0KoK1MKPXCRgzmksAMk79j4h3k1IerxTNqwDLxsGxStEZVBqG0gIqZqoA==}
cpu: [x64]
os: [win32]
'@rollup/rollup-win32-x64-msvc@4.57.0':
resolution: {integrity: sha512-Zv7v6q6aV+VslnpwzqKAmrk5JdVkLUzok2208ZXGipjb+msxBr/fJPZyeEXiFgH7k62Ak0SLIfxQRZQvTuf7rQ==}
cpu: [x64]
os: [win32]
'@standard-schema/spec@1.1.0':
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
'@types/chai@5.2.3':
resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
'@types/deep-eql@4.0.2':
resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
'@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
'@types/node@18.19.130':
resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==}
'@types/node@22.19.7':
resolution: {integrity: sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw==}
'@types/ws@8.18.1':
resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==}
'@vitest/expect@4.0.18':
resolution: {integrity: sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==}
'@vitest/mocker@4.0.18':
resolution: {integrity: sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==}
peerDependencies:
msw: ^2.4.9
vite: ^6.0.0 || ^7.0.0-0
peerDependenciesMeta:
msw:
optional: true
vite:
optional: true
'@vitest/pretty-format@4.0.18':
resolution: {integrity: sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==}
'@vitest/runner@4.0.18':
resolution: {integrity: sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==}
'@vitest/snapshot@4.0.18':
resolution: {integrity: sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==}
'@vitest/spy@4.0.18':
resolution: {integrity: sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==}
'@vitest/utils@4.0.18':
resolution: {integrity: sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==}
agent-base@6.0.2:
resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
engines: {node: '>= 6.0.0'}
assertion-error@2.0.1:
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
engines: {node: '>=12'}
asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
axios@1.13.4:
resolution: {integrity: sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg==}
buffer-equal-constant-time@1.0.1:
resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
call-bind-apply-helpers@1.0.2:
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
engines: {node: '>= 0.4'}
call-bound@1.0.4:
resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==}
engines: {node: '>= 0.4'}
chai@6.2.2:
resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==}
engines: {node: '>=18'}
combined-stream@1.0.8:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'}
command-exists@1.2.9:
resolution: {integrity: sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==}
commander@14.0.2:
resolution: {integrity: sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==}
engines: {node: '>=20'}
cross-fetch@3.2.0:
resolution: {integrity: sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==}
dayjs@1.11.19:
resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==}
debug@4.4.3:
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
engines: {node: '>=6.0'}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
deepmerge@4.3.1:
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
engines: {node: '>=0.10.0'}
delayed-stream@1.0.0:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'}
dunder-proto@1.0.1:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'}
ecdsa-sig-formatter@1.0.11:
resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==}
es-define-property@1.0.1:
resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
engines: {node: '>= 0.4'}
es-errors@1.3.0:
resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
engines: {node: '>= 0.4'}
es-module-lexer@1.7.0:
resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==}
es-object-atoms@1.1.1:
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
engines: {node: '>= 0.4'}
es-set-tostringtag@2.1.0:
resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
engines: {node: '>= 0.4'}
esbuild@0.27.2:
resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==}
engines: {node: '>=18'}
hasBin: true
estree-walker@3.0.3:
resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
events@3.3.0:
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
engines: {node: '>=0.8.x'}
expect-type@1.3.0:
resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==}
engines: {node: '>=12.0.0'}
fdir@6.5.0:
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
engines: {node: '>=12.0.0'}
peerDependencies:
picomatch: ^3 || ^4
peerDependenciesMeta:
picomatch:
optional: true
follow-redirects@1.15.11:
resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==}
engines: {node: '>=4.0'}
peerDependencies:
debug: '*'
peerDependenciesMeta:
debug:
optional: true
form-data@4.0.5:
resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
engines: {node: '>= 6'}
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
get-intrinsic@1.3.0:
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
engines: {node: '>= 0.4'}
get-proto@1.0.1:
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
engines: {node: '>= 0.4'}
gopd@1.2.0:
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
engines: {node: '>= 0.4'}
has-symbols@1.1.0:
resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
engines: {node: '>= 0.4'}
has-tostringtag@1.0.2:
resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
engines: {node: '>= 0.4'}
hasown@2.0.2:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
https-proxy-agent@5.0.1:
resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
engines: {node: '>= 6'}
json-schema-to-ts@3.1.1:
resolution: {integrity: sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==}
engines: {node: '>=16'}
json5@2.2.3:
resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
engines: {node: '>=6'}
hasBin: true
jsonwebtoken@9.0.3:
resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==}
engines: {node: '>=12', npm: '>=6'}
jwa@2.0.1:
resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==}
jws@4.0.1:
resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==}
kleur@4.1.5:
resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==}
engines: {node: '>=6'}
lodash.includes@4.3.0:
resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==}
lodash.isboolean@3.0.3:
resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==}
lodash.isinteger@4.0.4:
resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==}
lodash.isnumber@3.0.3:
resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==}
lodash.isplainobject@4.0.6:
resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==}
lodash.isstring@4.0.1:
resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==}
lodash.once@4.1.1:
resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==}
magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
math-intrinsics@1.1.0:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'}
mime-db@1.52.0:
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
engines: {node: '>= 0.6'}
mime-types@2.1.35:
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
engines: {node: '>= 0.6'}
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
nanoid@3.3.11:
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
node-fetch@2.7.0:
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
engines: {node: 4.x || >=6.0.0}
peerDependencies:
encoding: ^0.1.0
peerDependenciesMeta:
encoding:
optional: true
object-inspect@1.13.4:
resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
engines: {node: '>= 0.4'}
obug@2.1.1:
resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==}
pathe@2.0.3:
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
picomatch@4.0.3:
resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
engines: {node: '>=12'}
postcss@8.5.6:
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
engines: {node: ^10 || ^12 || >=14}
proxy-from-env@1.1.0:
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
qs@6.14.1:
resolution: {integrity: sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==}
engines: {node: '>=0.6'}
rollup@4.57.0:
resolution: {integrity: sha512-e5lPJi/aui4TO1LpAXIRLySmwXSE8k3b9zoGfd42p67wzxog4WHjiZF3M2uheQih4DGyc25QEV4yRBbpueNiUA==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true
safe-buffer@5.2.1:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
scmp@2.1.0:
resolution: {integrity: sha512-o/mRQGk9Rcer/jEEw/yw4mwo3EU/NvYvp577/Btqrym9Qy5/MdWGBqipbALgd2lrdWTJ5/gqDusxfnQBxOxT2Q==}
deprecated: Just use Node.js's crypto.timingSafeEqual()
semver@7.7.3:
resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==}
engines: {node: '>=10'}
hasBin: true
side-channel-list@1.0.0:
resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==}
engines: {node: '>= 0.4'}
side-channel-map@1.0.1:
resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==}
engines: {node: '>= 0.4'}
side-channel-weakmap@1.0.2:
resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==}
engines: {node: '>= 0.4'}
side-channel@1.1.0:
resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==}
engines: {node: '>= 0.4'}
siginfo@2.0.0:
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
stackback@0.0.2:
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
std-env@3.10.0:
resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==}
tinybench@2.9.0:
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
tinyexec@1.0.2:
resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==}
engines: {node: '>=18'}
tinyglobby@0.2.15:
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
engines: {node: '>=12.0.0'}
tinyrainbow@3.0.3:
resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==}
engines: {node: '>=14.0.0'}
tr46@0.0.3:
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
ts-algebra@2.0.0:
resolution: {integrity: sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==}
twilio@5.12.0:
resolution: {integrity: sha512-ZAKnDKcWvJSb90xQS13QB5KQOeMJPzsRPHxZqju8i5ALg3D4hNwAF9bpytVTxTJV99BL4Rn6Un+ZtXjGeMpjvQ==}
engines: {node: '>=14.0'}
typescript@5.9.3:
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
engines: {node: '>=14.17'}
hasBin: true
undici-types@5.26.5:
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
undici-types@6.21.0:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
vite@7.3.1:
resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
peerDependencies:
'@types/node': ^20.19.0 || >=22.12.0
jiti: '>=1.21.0'
less: ^4.0.0
lightningcss: ^1.21.0
sass: ^1.70.0
sass-embedded: ^1.70.0
stylus: '>=0.54.8'
sugarss: ^5.0.0
terser: ^5.16.0
tsx: ^4.8.1
yaml: ^2.4.2
peerDependenciesMeta:
'@types/node':
optional: true
jiti:
optional: true
less:
optional: true
lightningcss:
optional: true
sass:
optional: true
sass-embedded:
optional: true
stylus:
optional: true
sugarss:
optional: true
terser:
optional: true
tsx:
optional: true
yaml:
optional: true
vitest@4.0.18:
resolution: {integrity: sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==}
engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0}
hasBin: true
peerDependencies:
'@edge-runtime/vm': '*'
'@opentelemetry/api': ^1.9.0
'@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0
'@vitest/browser-playwright': 4.0.18
'@vitest/browser-preview': 4.0.18
'@vitest/browser-webdriverio': 4.0.18
'@vitest/ui': 4.0.18
happy-dom: '*'
jsdom: '*'
peerDependenciesMeta:
'@edge-runtime/vm':
optional: true
'@opentelemetry/api':
optional: true
'@types/node':
optional: true
'@vitest/browser-playwright':
optional: true
'@vitest/browser-preview':
optional: true
'@vitest/browser-webdriverio':
optional: true
'@vitest/ui':
optional: true
happy-dom:
optional: true
jsdom:
optional: true
webidl-conversions@3.0.1:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
whatwg-url@5.0.0:
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
why-is-node-running@2.3.0:
resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==}
engines: {node: '>=8'}
hasBin: true
ws@8.19.0:
resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==}
engines: {node: '>=10.0.0'}
peerDependencies:
bufferutil: ^4.0.1
utf-8-validate: '>=5.0.2'
peerDependenciesMeta:
bufferutil:
optional: true
utf-8-validate:
optional: true
xmlbuilder@13.0.2:
resolution: {integrity: sha512-Eux0i2QdDYKbdbA6AM6xE4m6ZTZr4G4xF9kahI2ukSEMCzwce2eX9WlTI5J3s+NU7hpasFsr8hWIONae7LluAQ==}
engines: {node: '>=6.0'}
zod@4.3.6:
resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==}
snapshots:
'@anthropic-ai/sdk@0.72.0(zod@4.3.6)':
dependencies:
json-schema-to-ts: 3.1.1
optionalDependencies:
zod: 4.3.6
'@babel/runtime@7.28.6': {}
'@biomejs/biome@2.3.13':
optionalDependencies:
'@biomejs/cli-darwin-arm64': 2.3.13
'@biomejs/cli-darwin-x64': 2.3.13
'@biomejs/cli-linux-arm64': 2.3.13
'@biomejs/cli-linux-arm64-musl': 2.3.13
'@biomejs/cli-linux-x64': 2.3.13
'@biomejs/cli-linux-x64-musl': 2.3.13
'@biomejs/cli-win32-arm64': 2.3.13
'@biomejs/cli-win32-x64': 2.3.13
'@biomejs/cli-darwin-arm64@2.3.13':
optional: true
'@biomejs/cli-darwin-x64@2.3.13':
optional: true
'@biomejs/cli-linux-arm64-musl@2.3.13':
optional: true
'@biomejs/cli-linux-arm64@2.3.13':
optional: true
'@biomejs/cli-linux-x64-musl@2.3.13':
optional: true
'@biomejs/cli-linux-x64@2.3.13':
optional: true
'@biomejs/cli-win32-arm64@2.3.13':
optional: true
'@biomejs/cli-win32-x64@2.3.13':
optional: true
'@deepgram/captions@1.2.0':
dependencies:
dayjs: 1.11.19
'@deepgram/sdk@3.13.0':
dependencies:
'@deepgram/captions': 1.2.0
'@types/node': 18.19.130
cross-fetch: 3.2.0
deepmerge: 4.3.1
events: 3.3.0
ws: 8.19.0
transitivePeerDependencies:
- bufferutil
- encoding
- utf-8-validate
'@elevenlabs/elevenlabs-js@2.33.0':
dependencies:
command-exists: 1.2.9
node-fetch: 2.7.0
ws: 8.19.0
transitivePeerDependencies:
- bufferutil
- encoding
- utf-8-validate
'@esbuild/aix-ppc64@0.27.2':
optional: true
'@esbuild/android-arm64@0.27.2':
optional: true
'@esbuild/android-arm@0.27.2':
optional: true
'@esbuild/android-x64@0.27.2':
optional: true
'@esbuild/darwin-arm64@0.27.2':
optional: true
'@esbuild/darwin-x64@0.27.2':
optional: true
'@esbuild/freebsd-arm64@0.27.2':
optional: true
'@esbuild/freebsd-x64@0.27.2':
optional: true
'@esbuild/linux-arm64@0.27.2':
optional: true
'@esbuild/linux-arm@0.27.2':
optional: true
'@esbuild/linux-ia32@0.27.2':
optional: true
'@esbuild/linux-loong64@0.27.2':
optional: true
'@esbuild/linux-mips64el@0.27.2':
optional: true
'@esbuild/linux-ppc64@0.27.2':
optional: true
'@esbuild/linux-riscv64@0.27.2':
optional: true
'@esbuild/linux-s390x@0.27.2':
optional: true
'@esbuild/linux-x64@0.27.2':
optional: true
'@esbuild/netbsd-arm64@0.27.2':
optional: true
'@esbuild/netbsd-x64@0.27.2':
optional: true
'@esbuild/openbsd-arm64@0.27.2':
optional: true
'@esbuild/openbsd-x64@0.27.2':
optional: true
'@esbuild/openharmony-arm64@0.27.2':
optional: true
'@esbuild/sunos-x64@0.27.2':
optional: true
'@esbuild/win32-arm64@0.27.2':
optional: true
'@esbuild/win32-ia32@0.27.2':
optional: true
'@esbuild/win32-x64@0.27.2':
optional: true
'@jridgewell/sourcemap-codec@1.5.5': {}
'@rollup/rollup-android-arm-eabi@4.57.0':
optional: true
'@rollup/rollup-android-arm64@4.57.0':
optional: true
'@rollup/rollup-darwin-arm64@4.57.0':
optional: true
'@rollup/rollup-darwin-x64@4.57.0':
optional: true
'@rollup/rollup-freebsd-arm64@4.57.0':
optional: true
'@rollup/rollup-freebsd-x64@4.57.0':
optional: true
'@rollup/rollup-linux-arm-gnueabihf@4.57.0':
optional: true
'@rollup/rollup-linux-arm-musleabihf@4.57.0':
optional: true
'@rollup/rollup-linux-arm64-gnu@4.57.0':
optional: true
'@rollup/rollup-linux-arm64-musl@4.57.0':
optional: true
'@rollup/rollup-linux-loong64-gnu@4.57.0':
optional: true
'@rollup/rollup-linux-loong64-musl@4.57.0':
optional: true
'@rollup/rollup-linux-ppc64-gnu@4.57.0':
optional: true
'@rollup/rollup-linux-ppc64-musl@4.57.0':
optional: true
'@rollup/rollup-linux-riscv64-gnu@4.57.0':
optional: true
'@rollup/rollup-linux-riscv64-musl@4.57.0':
optional: true
'@rollup/rollup-linux-s390x-gnu@4.57.0':
optional: true
'@rollup/rollup-linux-x64-gnu@4.57.0':
optional: true
'@rollup/rollup-linux-x64-musl@4.57.0':
optional: true
'@rollup/rollup-openbsd-x64@4.57.0':
optional: true
'@rollup/rollup-openharmony-arm64@4.57.0':
optional: true
'@rollup/rollup-win32-arm64-msvc@4.57.0':
optional: true
'@rollup/rollup-win32-ia32-msvc@4.57.0':
optional: true
'@rollup/rollup-win32-x64-gnu@4.57.0':
optional: true
'@rollup/rollup-win32-x64-msvc@4.57.0':
optional: true
'@standard-schema/spec@1.1.0': {}
'@types/chai@5.2.3':
dependencies:
'@types/deep-eql': 4.0.2
assertion-error: 2.0.1
'@types/deep-eql@4.0.2': {}
'@types/estree@1.0.8': {}
'@types/node@18.19.130':
dependencies:
undici-types: 5.26.5
'@types/node@22.19.7':
dependencies:
undici-types: 6.21.0
'@types/ws@8.18.1':
dependencies:
'@types/node': 22.19.7
'@vitest/expect@4.0.18':
dependencies:
'@standard-schema/spec': 1.1.0
'@types/chai': 5.2.3
'@vitest/spy': 4.0.18
'@vitest/utils': 4.0.18
chai: 6.2.2
tinyrainbow: 3.0.3
'@vitest/mocker@4.0.18(vite@7.3.1(@types/node@22.19.7))':
dependencies:
'@vitest/spy': 4.0.18
estree-walker: 3.0.3
magic-string: 0.30.21
optionalDependencies:
vite: 7.3.1(@types/node@22.19.7)
'@vitest/pretty-format@4.0.18':
dependencies:
tinyrainbow: 3.0.3
'@vitest/runner@4.0.18':
dependencies:
'@vitest/utils': 4.0.18
pathe: 2.0.3
'@vitest/snapshot@4.0.18':
dependencies:
'@vitest/pretty-format': 4.0.18
magic-string: 0.30.21
pathe: 2.0.3
'@vitest/spy@4.0.18': {}
'@vitest/utils@4.0.18':
dependencies:
'@vitest/pretty-format': 4.0.18
tinyrainbow: 3.0.3
agent-base@6.0.2:
dependencies:
debug: 4.4.3
transitivePeerDependencies:
- supports-color
assertion-error@2.0.1: {}
asynckit@0.4.0: {}
axios@1.13.4:
dependencies:
follow-redirects: 1.15.11
form-data: 4.0.5
proxy-from-env: 1.1.0
transitivePeerDependencies:
- debug
buffer-equal-constant-time@1.0.1: {}
call-bind-apply-helpers@1.0.2:
dependencies:
es-errors: 1.3.0
function-bind: 1.1.2
call-bound@1.0.4:
dependencies:
call-bind-apply-helpers: 1.0.2
get-intrinsic: 1.3.0
chai@6.2.2: {}
combined-stream@1.0.8:
dependencies:
delayed-stream: 1.0.0
command-exists@1.2.9: {}
commander@14.0.2: {}
cross-fetch@3.2.0:
dependencies:
node-fetch: 2.7.0
transitivePeerDependencies:
- encoding
dayjs@1.11.19: {}
debug@4.4.3:
dependencies:
ms: 2.1.3
deepmerge@4.3.1: {}
delayed-stream@1.0.0: {}
dunder-proto@1.0.1:
dependencies:
call-bind-apply-helpers: 1.0.2
es-errors: 1.3.0
gopd: 1.2.0
ecdsa-sig-formatter@1.0.11:
dependencies:
safe-buffer: 5.2.1
es-define-property@1.0.1: {}
es-errors@1.3.0: {}
es-module-lexer@1.7.0: {}
es-object-atoms@1.1.1:
dependencies:
es-errors: 1.3.0
es-set-tostringtag@2.1.0:
dependencies:
es-errors: 1.3.0
get-intrinsic: 1.3.0
has-tostringtag: 1.0.2
hasown: 2.0.2
esbuild@0.27.2:
optionalDependencies:
'@esbuild/aix-ppc64': 0.27.2
'@esbuild/android-arm': 0.27.2
'@esbuild/android-arm64': 0.27.2
'@esbuild/android-x64': 0.27.2
'@esbuild/darwin-arm64': 0.27.2
'@esbuild/darwin-x64': 0.27.2
'@esbuild/freebsd-arm64': 0.27.2
'@esbuild/freebsd-x64': 0.27.2
'@esbuild/linux-arm': 0.27.2
'@esbuild/linux-arm64': 0.27.2
'@esbuild/linux-ia32': 0.27.2
'@esbuild/linux-loong64': 0.27.2
'@esbuild/linux-mips64el': 0.27.2
'@esbuild/linux-ppc64': 0.27.2
'@esbuild/linux-riscv64': 0.27.2
'@esbuild/linux-s390x': 0.27.2
'@esbuild/linux-x64': 0.27.2
'@esbuild/netbsd-arm64': 0.27.2
'@esbuild/netbsd-x64': 0.27.2
'@esbuild/openbsd-arm64': 0.27.2
'@esbuild/openbsd-x64': 0.27.2
'@esbuild/openharmony-arm64': 0.27.2
'@esbuild/sunos-x64': 0.27.2
'@esbuild/win32-arm64': 0.27.2
'@esbuild/win32-ia32': 0.27.2
'@esbuild/win32-x64': 0.27.2
estree-walker@3.0.3:
dependencies:
'@types/estree': 1.0.8
events@3.3.0: {}
expect-type@1.3.0: {}
fdir@6.5.0(picomatch@4.0.3):
optionalDependencies:
picomatch: 4.0.3
follow-redirects@1.15.11: {}
form-data@4.0.5:
dependencies:
asynckit: 0.4.0
combined-stream: 1.0.8
es-set-tostringtag: 2.1.0
hasown: 2.0.2
mime-types: 2.1.35
fsevents@2.3.3:
optional: true
function-bind@1.1.2: {}
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
get-proto@1.0.1:
dependencies:
dunder-proto: 1.0.1
es-object-atoms: 1.1.1
gopd@1.2.0: {}
has-symbols@1.1.0: {}
has-tostringtag@1.0.2:
dependencies:
has-symbols: 1.1.0
hasown@2.0.2:
dependencies:
function-bind: 1.1.2
https-proxy-agent@5.0.1:
dependencies:
agent-base: 6.0.2
debug: 4.4.3
transitivePeerDependencies:
- supports-color
json-schema-to-ts@3.1.1:
dependencies:
'@babel/runtime': 7.28.6
ts-algebra: 2.0.0
json5@2.2.3: {}
jsonwebtoken@9.0.3:
dependencies:
jws: 4.0.1
lodash.includes: 4.3.0
lodash.isboolean: 3.0.3
lodash.isinteger: 4.0.4
lodash.isnumber: 3.0.3
lodash.isplainobject: 4.0.6
lodash.isstring: 4.0.1
lodash.once: 4.1.1
ms: 2.1.3
semver: 7.7.3
jwa@2.0.1:
dependencies:
buffer-equal-constant-time: 1.0.1
ecdsa-sig-formatter: 1.0.11
safe-buffer: 5.2.1
jws@4.0.1:
dependencies:
jwa: 2.0.1
safe-buffer: 5.2.1
kleur@4.1.5: {}
lodash.includes@4.3.0: {}
lodash.isboolean@3.0.3: {}
lodash.isinteger@4.0.4: {}
lodash.isnumber@3.0.3: {}
lodash.isplainobject@4.0.6: {}
lodash.isstring@4.0.1: {}
lodash.once@4.1.1: {}
magic-string@0.30.21:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
math-intrinsics@1.1.0: {}
mime-db@1.52.0: {}
mime-types@2.1.35:
dependencies:
mime-db: 1.52.0
ms@2.1.3: {}
nanoid@3.3.11: {}
node-fetch@2.7.0:
dependencies:
whatwg-url: 5.0.0
object-inspect@1.13.4: {}
obug@2.1.1: {}
pathe@2.0.3: {}
picocolors@1.1.1: {}
picomatch@4.0.3: {}
postcss@8.5.6:
dependencies:
nanoid: 3.3.11
picocolors: 1.1.1
source-map-js: 1.2.1
proxy-from-env@1.1.0: {}
qs@6.14.1:
dependencies:
side-channel: 1.1.0
rollup@4.57.0:
dependencies:
'@types/estree': 1.0.8
optionalDependencies:
'@rollup/rollup-android-arm-eabi': 4.57.0
'@rollup/rollup-android-arm64': 4.57.0
'@rollup/rollup-darwin-arm64': 4.57.0
'@rollup/rollup-darwin-x64': 4.57.0
'@rollup/rollup-freebsd-arm64': 4.57.0
'@rollup/rollup-freebsd-x64': 4.57.0
'@rollup/rollup-linux-arm-gnueabihf': 4.57.0
'@rollup/rollup-linux-arm-musleabihf': 4.57.0
'@rollup/rollup-linux-arm64-gnu': 4.57.0
'@rollup/rollup-linux-arm64-musl': 4.57.0
'@rollup/rollup-linux-loong64-gnu': 4.57.0
'@rollup/rollup-linux-loong64-musl': 4.57.0
'@rollup/rollup-linux-ppc64-gnu': 4.57.0
'@rollup/rollup-linux-ppc64-musl': 4.57.0
'@rollup/rollup-linux-riscv64-gnu': 4.57.0
'@rollup/rollup-linux-riscv64-musl': 4.57.0
'@rollup/rollup-linux-s390x-gnu': 4.57.0
'@rollup/rollup-linux-x64-gnu': 4.57.0
'@rollup/rollup-linux-x64-musl': 4.57.0
'@rollup/rollup-openbsd-x64': 4.57.0
'@rollup/rollup-openharmony-arm64': 4.57.0
'@rollup/rollup-win32-arm64-msvc': 4.57.0
'@rollup/rollup-win32-ia32-msvc': 4.57.0
'@rollup/rollup-win32-x64-gnu': 4.57.0
'@rollup/rollup-win32-x64-msvc': 4.57.0
fsevents: 2.3.3
safe-buffer@5.2.1: {}
scmp@2.1.0: {}
semver@7.7.3: {}
side-channel-list@1.0.0:
dependencies:
es-errors: 1.3.0
object-inspect: 1.13.4
side-channel-map@1.0.1:
dependencies:
call-bound: 1.0.4
es-errors: 1.3.0
get-intrinsic: 1.3.0
object-inspect: 1.13.4
side-channel-weakmap@1.0.2:
dependencies:
call-bound: 1.0.4
es-errors: 1.3.0
get-intrinsic: 1.3.0
object-inspect: 1.13.4
side-channel-map: 1.0.1
side-channel@1.1.0:
dependencies:
es-errors: 1.3.0
object-inspect: 1.13.4
side-channel-list: 1.0.0
side-channel-map: 1.0.1
side-channel-weakmap: 1.0.2
siginfo@2.0.0: {}
source-map-js@1.2.1: {}
stackback@0.0.2: {}
std-env@3.10.0: {}
tinybench@2.9.0: {}
tinyexec@1.0.2: {}
tinyglobby@0.2.15:
dependencies:
fdir: 6.5.0(picomatch@4.0.3)
picomatch: 4.0.3
tinyrainbow@3.0.3: {}
tr46@0.0.3: {}
ts-algebra@2.0.0: {}
twilio@5.12.0:
dependencies:
axios: 1.13.4
dayjs: 1.11.19
https-proxy-agent: 5.0.1
jsonwebtoken: 9.0.3
qs: 6.14.1
scmp: 2.1.0
xmlbuilder: 13.0.2
transitivePeerDependencies:
- debug
- supports-color
typescript@5.9.3: {}
undici-types@5.26.5: {}
undici-types@6.21.0: {}
vite@7.3.1(@types/node@22.19.7):
dependencies:
esbuild: 0.27.2
fdir: 6.5.0(picomatch@4.0.3)
picomatch: 4.0.3
postcss: 8.5.6
rollup: 4.57.0
tinyglobby: 0.2.15
optionalDependencies:
'@types/node': 22.19.7
fsevents: 2.3.3
vitest@4.0.18(@types/node@22.19.7):
dependencies:
'@vitest/expect': 4.0.18
'@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@22.19.7))
'@vitest/pretty-format': 4.0.18
'@vitest/runner': 4.0.18
'@vitest/snapshot': 4.0.18
'@vitest/spy': 4.0.18
'@vitest/utils': 4.0.18
es-module-lexer: 1.7.0
expect-type: 1.3.0
magic-string: 0.30.21
obug: 2.1.1
pathe: 2.0.3
picomatch: 4.0.3
std-env: 3.10.0
tinybench: 2.9.0
tinyexec: 1.0.2
tinyglobby: 0.2.15
tinyrainbow: 3.0.3
vite: 7.3.1(@types/node@22.19.7)
why-is-node-running: 2.3.0
optionalDependencies:
'@types/node': 22.19.7
transitivePeerDependencies:
- jiti
- less
- lightningcss
- msw
- sass
- sass-embedded
- stylus
- sugarss
- terser
- tsx
- yaml
webidl-conversions@3.0.1: {}
whatwg-url@5.0.0:
dependencies:
tr46: 0.0.3
webidl-conversions: 3.0.1
why-is-node-running@2.3.0:
dependencies:
siginfo: 2.0.0
stackback: 0.0.2
ws@8.19.0: {}
xmlbuilder@13.0.2: {}
zod@4.3.6:
optional: true
Concierge
Personal AI concierge for contact lookup, availability checks, and autonomous phone calls.
Installation
skill install @skillhq/conciergeFeatures
- Contact Extraction: Find phone, email, WhatsApp, and social media contacts from listings and businesses (Airbnb, Booking.com, VRBO, Expedia)
- AI Phone Calls: Place autonomous phone calls with a goal-driven AI agent that handles the conversation until the goal is achieved
System Dependencies (AI Calls)
AI calls require local binaries in addition to API keys:
ffmpeg(must include MP3 decode support)ngrok(used whencallauto-starts infrastructure)
Install examples:
# macOS
brew install ffmpeg ngrok
# Ubuntu/Debian (including ARM VPS)
sudo apt-get update
sudo apt-get install -y ffmpeg
# install ngrok from ngrok docs, then verify `ngrok version`Verify locally:
ffmpeg -version
ffmpeg -decoders | rg -i mp3
ngrok versionThe call preflight now fails fast with a clear error if these dependencies are missing or if ffmpeg cannot decode MP3.
Quick Start
Find contacts for a listing
concierge find-contact "https://www.airbnb.com/rooms/12345"Place an AI booking call
concierge call "+1-555-123-4567" \
--goal "Book a room for March 12-14" \
--name "John Smith" \
--email "john@example.com" \
--customer-phone "+1-555-000-1111"Configuration
Configuration is stored in ~/.config/concierge/config.json5.
For contact lookup (optional)
concierge config set googlePlacesApiKey "your-key"For AI phone calls (required)
concierge config set twilioAccountSid "<sid>"
concierge config set twilioAuthToken "<token>"
concierge config set twilioPhoneNumber "+14155551234"
concierge config set deepgramApiKey "<key>"
concierge config set elevenLabsApiKey "<key>"
concierge config set elevenLabsVoiceId "EXAVITQu4vr4xnSDxMaL"
concierge config set anthropicApiKey "<key>"Documentation
See SKILL.md and CALL-SETUP.md for full setup and troubleshooting.
License
MIT
#!/usr/bin/env bash
set -euo pipefail
# Publish concierge skill to ClawdHub
# Usage:
# ./scripts/publish-skill.sh # Use version from package.json
# ./scripts/publish-skill.sh 0.2.0 # Specify version
# ./scripts/publish-skill.sh patch # Bump patch (0.1.0 -> 0.1.1)
# ./scripts/publish-skill.sh minor # Bump minor (0.1.0 -> 0.2.0)
# ./scripts/publish-skill.sh major # Bump major (0.1.0 -> 1.0.0)
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
cd "$PROJECT_DIR"
# Read current version from package.json
CURRENT_VERSION=$(node -p "require('./package.json').version")
# Determine target version
if [[ $# -eq 0 ]]; then
VERSION="$CURRENT_VERSION"
elif [[ "$1" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
VERSION="$1"
elif [[ "$1" == "patch" || "$1" == "minor" || "$1" == "major" ]]; then
IFS='.' read -r major minor patch <<< "$CURRENT_VERSION"
case "$1" in
patch) VERSION="$major.$minor.$((patch + 1))" ;;
minor) VERSION="$major.$((minor + 1)).0" ;;
major) VERSION="$((major + 1)).0.0" ;;
esac
else
echo "Usage: $0 [version|patch|minor|major]"
echo " version: semver like 0.2.0"
echo " patch: bump patch version"
echo " minor: bump minor version"
echo " major: bump major version"
exit 1
fi
# Get changelog from git (commits since last tag, or last 5 commits)
LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
if [[ -n "$LAST_TAG" ]]; then
CHANGELOG=$(git log "$LAST_TAG"..HEAD --oneline --no-decorate | head -10 | sed 's/^/- /')
else
CHANGELOG=$(git log -5 --oneline --no-decorate | sed 's/^/- /')
fi
if [[ -z "$CHANGELOG" ]]; then
CHANGELOG="Version $VERSION"
fi
echo "Publishing concierge skill v$VERSION"
echo "Changelog:"
echo "$CHANGELOG"
echo ""
# Update package.json version if different
if [[ "$VERSION" != "$CURRENT_VERSION" ]]; then
echo "Updating package.json version: $CURRENT_VERSION -> $VERSION"
npm pkg set version="$VERSION"
fi
# Publish to ClawdHub
# Note: --workdir is required to override clawdbot default workspace
clawdhub --workdir "$PROJECT_DIR" publish . \
--slug concierge \
--name "Travel Concierge CLI" \
--version "$VERSION" \
--tags latest \
--changelog "$CHANGELOG"
echo ""
echo "Published concierge@$VERSION to ClawdHub"
#!/usr/bin/env node
import { createProgram } from './cli/program.js';
const program = createProgram();
program.parse();
import { createRequire } from 'node:module';
import { Command } from 'commander';
import kleur from 'kleur';
import { callCommand } from '../commands/call.js';
import { checkAvailabilityCommand } from '../commands/check-availability.js';
import { configCommand } from '../commands/config.js';
import { findCommand } from '../commands/find.js';
import { flightqueueCommand } from '../commands/flightqueue.js';
import { serverCommand } from '../commands/server.js';
import type { CliColors, CliContext } from './shared.js';
const require = createRequire(import.meta.url);
const pkg = require('../../package.json') as { version: string };
function createColors(): CliColors {
return {
primary: kleur.cyan,
secondary: kleur.magenta,
success: kleur.green,
error: kleur.red,
warning: kleur.yellow,
info: kleur.blue,
muted: kleur.gray,
highlight: kleur.bold,
};
}
export function createProgram(): Command {
const program = new Command();
program
.name('concierge')
.description('Personal AI concierge for contact lookup, availability checks, and autonomous phone calls')
.version(pkg.version);
// Global options
program.option('--json', 'Output as JSON').option('-v, --verbose', 'Verbose output');
// Create shared context
const getContext = (): CliContext => {
const opts = program.opts();
return {
colors: createColors(),
json: opts.json ?? false,
verbose: opts.verbose ?? false,
};
};
// Register commands
findCommand(program, getContext);
flightqueueCommand(program, getContext);
configCommand(program, getContext);
serverCommand(program, getContext);
callCommand(program, getContext);
checkAvailabilityCommand(program, getContext);
return program;
}
// kleur color function type
export type ColorFn = (text: string | number) => string;
export interface CliColors {
primary: ColorFn;
secondary: ColorFn;
success: ColorFn;
error: ColorFn;
warning: ColorFn;
info: ColorFn;
muted: ColorFn;
highlight: ColorFn;
}
export interface CliContext {
colors: CliColors;
json: boolean;
verbose: boolean;
}
export type OutputMode = 'text' | 'json';
/**
* Call command - Make phone calls via the voice call server
*/
import type { ChildProcess } from 'node:child_process';
import { execSync, spawn } from 'node:child_process';
import { randomUUID } from 'node:crypto';
import { once } from 'node:events';
import { createWriteStream, mkdirSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
import { homedir } from 'node:os';
import { join, resolve } from 'node:path';
import * as readline from 'node:readline';
import type { Command } from 'commander';
import { WebSocket } from 'ws';
import type { CliContext } from '../cli/shared.js';
import type { ClientMessage, ServerMessage } from '../lib/call/index.js';
import { preflightNgrok } from '../lib/call/providers/local-deps.js';
import { loadConfig } from '../lib/config.js';
const RECORDING_POLL_INTERVAL_MS = 3000;
const RECORDING_POLL_MAX_ATTEMPTS = 20; // 60 seconds max wait
const NGROK_START_TIMEOUT_MS = 20000;
const SERVER_START_TIMEOUT_MS = 25000;
const PUBLIC_WEBHOOK_STABILITY_WINDOW_MS = 25000;
const PUBLIC_WEBHOOK_STABILITY_PROBE_INTERVAL_MS = 2500;
interface ManagedInfraRuntime {
enabled: boolean;
ngrok?: ChildProcess;
server?: ChildProcess;
logDir?: string;
ngrokLogPath?: string;
serverLogPath?: string;
}
function delay(ms: number): Promise<void> {
return new Promise((resolveDelay) => setTimeout(resolveDelay, ms));
}
async function fetchAndSaveRecording(
port: number,
callSid: string,
logDir: string,
colors: CliContext['colors'],
): Promise<string | null> {
console.log(colors.muted('Fetching call recording...'));
for (let attempt = 0; attempt < RECORDING_POLL_MAX_ATTEMPTS; attempt++) {
try {
// First check if recordings are available
const metaResponse = await fetch(`http://localhost:${port}/recordings/${callSid}`);
if (!metaResponse.ok) {
throw new Error(`Server returned ${metaResponse.status}`);
}
const meta = (await metaResponse.json()) as { recordings: { sid: string; duration: number }[] };
if (!meta.recordings || meta.recordings.length === 0 || meta.recordings[0].duration < 0) {
if (attempt < RECORDING_POLL_MAX_ATTEMPTS - 1) {
await delay(RECORDING_POLL_INTERVAL_MS);
continue;
}
console.log(colors.warning('No recording available (call may have been too short)'));
return null;
}
// Download the recording
const downloadResponse = await fetch(`http://localhost:${port}/recordings/${callSid}?download=true`);
if (!downloadResponse.ok) {
throw new Error(`Download failed: ${downloadResponse.status}`);
}
const audioBuffer = await downloadResponse.arrayBuffer();
const recordingPath = join(logDir, `recording-${callSid}.wav`);
const rawPath = join(logDir, `recording-${callSid}.raw.wav`);
writeFileSync(rawPath, Buffer.from(audioBuffer));
// Twilio WAV files are mu-law encoded — convert to PCM WAV for standard playback
try {
execSync(`ffmpeg -y -i "${rawPath}" -acodec pcm_s16le "${recordingPath}" 2>/dev/null`, { stdio: 'pipe' });
try {
unlinkSync(rawPath);
} catch {}
} catch {
// ffmpeg unavailable or failed — fall back to the raw Twilio WAV
renameSync(rawPath, recordingPath);
}
console.log(colors.success(`Recording saved: ${recordingPath}`));
return recordingPath;
} catch (error) {
if (attempt < RECORDING_POLL_MAX_ATTEMPTS - 1) {
await delay(RECORDING_POLL_INTERVAL_MS);
continue;
}
const msg = error instanceof Error ? error.message : String(error);
console.log(colors.warning(`Could not fetch recording: ${msg}`));
return null;
}
}
return null;
}
function getCliEntryPath(): string {
if (process.argv[1]) {
return resolve(process.argv[1]);
}
return resolve(process.cwd(), 'dist', 'cli.js');
}
function createInfraLogPaths(
baseDir?: string,
phone?: string,
): { logDir: string; ngrokLogPath: string; serverLogPath: string } {
const sanitizedPhone = phone ? phone.replace(/[^+\dA-Za-z]/g, '') : undefined;
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const suffix = randomUUID().slice(0, 8);
const runId = sanitizedPhone ? `${timestamp}_${sanitizedPhone}-${suffix}` : `${timestamp}-${suffix}`;
const base = baseDir ?? join(homedir(), '.config', 'concierge', 'call-runs');
const logDir = join(base, runId);
mkdirSync(logDir, { recursive: true });
return {
logDir,
ngrokLogPath: join(logDir, 'ngrok.log'),
serverLogPath: join(logDir, 'server.log'),
};
}
function pipeProcessLogs(processRef: ChildProcess, logPath: string): void {
const stream = createWriteStream(logPath, { flags: 'a' });
if (processRef.stdout) {
processRef.stdout.on('data', (chunk) => stream.write(chunk));
}
if (processRef.stderr) {
processRef.stderr.on('data', (chunk) => stream.write(chunk));
}
processRef.on('close', () => {
stream.end();
});
}
async function waitForExit(processRef: ChildProcess): Promise<void> {
if (processRef.exitCode !== null) return;
await once(processRef, 'exit');
}
async function stopProcess(processRef: ChildProcess | undefined, timeoutMs: number): Promise<void> {
if (!processRef || processRef.exitCode !== null) return;
processRef.kill('SIGTERM');
await Promise.race([waitForExit(processRef), delay(timeoutMs)]);
if (processRef.exitCode === null) {
processRef.kill('SIGKILL');
await waitForExit(processRef);
}
}
async function isServerReachable(port: number, timeoutMs: number): Promise<boolean> {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(`http://localhost:${port}/health`, {
signal: controller.signal,
});
return response.ok;
} catch {
return false;
} finally {
clearTimeout(timer);
}
}
async function waitForServerReady(port: number, processRef: ChildProcess, timeoutMs: number): Promise<void> {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
if (processRef.exitCode !== null) {
throw new Error(`Server process exited early with code ${processRef.exitCode}.`);
}
if (await isServerReachable(port, 1200)) {
return;
}
await delay(300);
}
throw new Error(`Timed out waiting for local call server on port ${port}.`);
}
async function waitForNgrokPublicUrl(processRef: ChildProcess, timeoutMs: number): Promise<string> {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
if (processRef.exitCode !== null) {
throw new Error(`ngrok process exited early with code ${processRef.exitCode}.`);
}
try {
const response = await fetch('http://127.0.0.1:4040/api/tunnels');
if (response.ok) {
const payload = (await response.json()) as {
tunnels?: Array<{ public_url?: string; proto?: string }>;
};
const httpsTunnel = payload.tunnels?.find((tunnel) => tunnel.proto === 'https' && !!tunnel.public_url);
if (httpsTunnel?.public_url) {
return httpsTunnel.public_url;
}
}
} catch {
// Keep polling until timeout.
}
await delay(300);
}
throw new Error('Timed out waiting for ngrok tunnel URL.');
}
async function startManagedInfra(
port: number,
ngrokAuthToken: string | undefined,
logBaseDir?: string,
phone?: string,
): Promise<ManagedInfraRuntime & { publicUrl: string }> {
const runtime: ManagedInfraRuntime = { enabled: true };
const { logDir, ngrokLogPath, serverLogPath } = createInfraLogPaths(logBaseDir, phone);
runtime.logDir = logDir;
runtime.ngrokLogPath = ngrokLogPath;
runtime.serverLogPath = serverLogPath;
try {
const ngrokEnv = { ...process.env };
if (ngrokAuthToken) {
ngrokEnv.NGROK_AUTHTOKEN = ngrokAuthToken;
}
runtime.ngrok = spawn('ngrok', ['http', String(port), '--log', 'stdout'], {
env: ngrokEnv,
stdio: ['ignore', 'pipe', 'pipe'],
});
pipeProcessLogs(runtime.ngrok, ngrokLogPath);
const publicUrl = await waitForNgrokPublicUrl(runtime.ngrok, NGROK_START_TIMEOUT_MS);
runtime.server = spawn(
process.execPath,
[getCliEntryPath(), 'server', 'start', '--port', String(port), '--public-url', publicUrl],
{ stdio: ['ignore', 'pipe', 'pipe'] },
);
pipeProcessLogs(runtime.server, serverLogPath);
await waitForServerReady(port, runtime.server, SERVER_START_TIMEOUT_MS);
await verifyPublicWebhookStability(publicUrl, PUBLIC_WEBHOOK_STABILITY_WINDOW_MS);
return { ...runtime, publicUrl };
} catch (error) {
await stopProcess(runtime.server, 2000);
await stopProcess(runtime.ngrok, 2000);
throw error;
}
}
async function verifyPublicWebhookStability(publicUrl: string, durationMs: number): Promise<void> {
const deadline = Date.now() + durationMs;
let probe = 0;
while (Date.now() < deadline) {
probe += 1;
const healthResponse = await fetch(`${publicUrl}/health`);
if (!healthResponse.ok) {
throw new Error(`Public webhook stability check failed: ${publicUrl}/health returned ${healthResponse.status}.`);
}
const statusResponse = await fetch(`${publicUrl}/twilio/status?callId=preflight-stability-${probe}`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: 'CallSid=CApreflight&CallStatus=ringing',
});
if (!statusResponse.ok) {
throw new Error(
`Public webhook stability check failed: ${publicUrl}/twilio/status returned ${statusResponse.status}.`,
);
}
await delay(PUBLIC_WEBHOOK_STABILITY_PROBE_INTERVAL_MS);
}
}
async function stopManagedInfra(runtime: ManagedInfraRuntime): Promise<void> {
if (!runtime.enabled) return;
await stopProcess(runtime.server, 3000);
await stopProcess(runtime.ngrok, 3000);
}
function phoneticSpelling(name: string): string {
return name
.split(/\s+/)
.map((w) => `${w.split('').join(', ').toUpperCase()}.`)
.join(' ');
}
const DIGIT_WORDS: Record<string, string> = {
'0': 'zero',
'1': 'one',
'2': 'two',
'3': 'three',
'4': 'four',
'5': 'five',
'6': 'six',
'7': 'seven',
'8': 'eight',
'9': 'nine',
};
function phoneForSpeech(phone: string): string {
const digits = phone.replace(/[^0-9+]/g, '');
let prefix = '';
let i = 0;
if (digits.startsWith('+')) {
prefix = 'plus ';
i = 1;
}
const digitsPart = digits.slice(i);
const groups: string[] = [];
for (let j = 0; j < digitsPart.length; j += 3) {
groups.push(
digitsPart
.slice(j, j + 3)
.split('')
.map((d) => DIGIT_WORDS[d] ?? d)
.join(' '),
);
}
return prefix + groups.join(', ');
}
function formatDateForSpeech(dateStr: string): string {
const [year, month, day] = dateStr.split('-').map(Number);
const monthNames = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
];
return `${monthNames[month - 1]} ${day}, ${year}`;
}
interface CallOptions {
goal: string;
name: string;
email: string;
customerPhone: string;
context?: string;
port?: string;
outputDir?: string;
interactive: boolean;
autoInfra: boolean;
}
interface DirectBookingOptions {
hotel: string;
checkIn: string;
checkOut: string;
room?: string;
bookingPrice?: string;
currency?: string;
discount?: string;
context?: string;
port?: string;
outputDir?: string;
interactive: boolean;
autoInfra: boolean;
}
async function executeCall(phone: string, options: CallOptions, ctx: CliContext): Promise<void> {
const { colors } = ctx;
const config = loadConfig();
const port = options.port ? Number.parseInt(options.port, 10) : (config.callServerPort ?? 3000);
if (Number.isNaN(port) || port < 1 || port > 65535) {
console.log(colors.error(`Invalid port: ${options.port}`));
process.exit(1);
}
let runtime: ManagedInfraRuntime = { enabled: false };
const callOutputBase = options.outputDir ?? config.callOutputDir ?? undefined;
// Log directory for call artifacts (transcript, recording) — set below
let logDir: string | undefined;
try {
let serverReady = await isServerReachable(port, 1000);
if (!serverReady) {
if (!options.autoInfra) {
console.log(colors.error('Call server is not running and auto-infra is disabled.'));
console.log(colors.info('Start manually with:'));
console.log(colors.muted(' concierge server start --public-url <ngrok-url>'));
process.exit(1);
}
const ngrokPreflight = await preflightNgrok();
if (!ngrokPreflight.ok) {
throw new Error(ngrokPreflight.message);
}
console.log(colors.info(`[Preflight] ${ngrokPreflight.message}`));
console.log(colors.info('Starting managed infrastructure (ngrok + server)...'));
runtime = await startManagedInfra(port, config.ngrokAuthToken, callOutputBase, phone);
logDir = runtime.logDir!;
serverReady = true;
console.log(colors.highlight('Infrastructure Logs:'));
if (runtime.serverLogPath) {
console.log(colors.muted(` Server: ${runtime.serverLogPath}`));
}
if (runtime.ngrokLogPath) {
console.log(colors.muted(` Ngrok: ${runtime.ngrokLogPath}`));
}
console.log('');
}
if (!serverReady) {
throw new Error(`Unable to reach call server on port ${port}`);
}
// When server was already running, create a standalone log dir
if (!logDir) {
logDir = createInfraLogPaths(callOutputBase, phone).logDir;
}
console.log(colors.info('Connecting to call server...'));
console.log(colors.muted(`Call logs: ${logDir}`));
await runCallOverControlSocket(phone, options, port, ctx, logDir);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.error(colors.error(`Call failed: ${message}`));
process.exitCode = 1;
} finally {
await stopManagedInfra(runtime);
}
}
function runCallOverControlSocket(
phone: string,
options: CallOptions,
port: number,
ctx: CliContext,
logDir: string,
): Promise<void> {
const { colors } = ctx;
const ws = new WebSocket(`ws://localhost:${port}/control`);
let callId: string | null = null;
let callEnded = false;
let opened = false;
let done = false;
let rlInterface: readline.Interface | null = null;
const transcriptLines: string[] = [];
const transcriptPath = join(logDir, 'transcript.txt');
const flushTranscript = () => {
if (transcriptLines.length > 0) {
writeFileSync(transcriptPath, `${transcriptLines.join('\n')}\n`);
}
};
return new Promise<void>((resolve, reject) => {
const cleanup = () => {
if (done) return;
done = true;
if (rlInterface) {
rlInterface.close();
rlInterface = null;
}
if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
ws.close();
}
process.off('SIGINT', onShutdown);
process.off('SIGTERM', onShutdown);
};
const safeResolve = () => {
if (!done) {
cleanup();
}
resolve();
};
const safeReject = (error: Error) => {
if (!done) {
cleanup();
}
reject(error);
};
ws.on('open', () => {
opened = true;
console.log(colors.success('Connected to server'));
console.log('');
console.log(colors.highlight('Call Details:'));
console.log(colors.muted(` Phone: ${phone}`));
console.log(colors.muted(` Goal: ${options.goal}`));
console.log(colors.muted(` Customer: ${options.name}`));
console.log(colors.muted(` Email: ${options.email}`));
console.log(colors.muted(` Customer Phone: ${options.customerPhone}`));
if (options.context) {
console.log(colors.muted(` Context: ${options.context}`));
}
console.log('');
const customerContext = `Customer: ${options.name} (to spell: ${phoneticSpelling(options.name)})
Email: ${options.email}
Phone (to read aloud): ${phoneForSpeech(options.customerPhone)}${options.context ? `\n${options.context}` : ''}`;
const msg: ClientMessage = {
type: 'initiate_call',
phoneNumber: phone,
goal: options.goal,
context: customerContext,
};
ws.send(JSON.stringify(msg));
});
ws.on('message', (data) => {
try {
const msg = JSON.parse(data.toString()) as ServerMessage;
switch (msg.type) {
case 'call_started':
callId = msg.callId;
console.log(colors.info(`Call initiated (ID: ${msg.callId})`));
console.log(colors.muted('Waiting for answer...'));
break;
case 'call_ringing':
console.log(colors.info('Phone ringing...'));
break;
case 'call_connected':
console.log(colors.success('Call connected!'));
console.log('');
if (options.interactive) {
console.log(colors.highlight('Interactive mode - type messages to speak'));
console.log(colors.muted('Commands: /hangup, /status'));
console.log('');
rlInterface = readline.createInterface({
input: process.stdin,
output: process.stdout,
prompt: colors.muted('> '),
});
rlInterface.prompt();
rlInterface.on('line', (line: string) => {
const input = line.trim();
if (!input) {
rlInterface?.prompt();
return;
}
if (input === '/hangup') {
if (callId) {
ws.send(JSON.stringify({ type: 'hangup', callId } satisfies ClientMessage));
console.log(colors.info('Hanging up...'));
}
} else if (input === '/status') {
console.log(colors.info(`Call ID: ${callId}`));
} else if (input.startsWith('/')) {
console.log(colors.warning(`Unknown command: ${input}`));
} else if (callId) {
ws.send(JSON.stringify({ type: 'speak', callId, text: input } satisfies ClientMessage));
}
rlInterface?.prompt();
});
} else {
console.log(colors.highlight('Transcript:'));
console.log(colors.muted('─'.repeat(50)));
}
break;
case 'transcript':
if (msg.isFinal) {
const label = msg.role === 'assistant' ? colors.primary('AI') : colors.secondary('Human');
console.log(`${label}: ${msg.text}`);
const rawLabel = msg.role === 'assistant' ? 'AI' : 'Human';
transcriptLines.push(`${rawLabel}: ${msg.text}`);
flushTranscript();
}
break;
case 'call_ended': {
callEnded = true;
console.log('');
console.log(colors.muted('─'.repeat(50)));
console.log(colors.info(`Call ended (status: ${msg.status})`));
console.log('');
console.log(colors.highlight('Summary:'));
console.log(msg.summary);
// Save transcript with summary to log dir
transcriptLines.push('─'.repeat(50));
transcriptLines.push('Summary:');
transcriptLines.push(msg.summary);
flushTranscript();
console.log('');
console.log(colors.muted(`Transcript saved: ${transcriptPath}`));
// Fetch and save recording if call was answered (skip for busy/no-answer/failed/canceled)
const recordableStatuses = new Set(['completed', 'in-progress']);
if (msg.callSid && recordableStatuses.has(msg.status)) {
fetchAndSaveRecording(port, msg.callSid, logDir, colors)
.then(() => safeResolve())
.catch(() => safeResolve());
} else {
safeResolve();
}
break;
}
case 'error':
console.log(colors.error(`Error: ${msg.message}`));
if (!callId) {
safeReject(new Error(msg.message));
}
break;
}
} catch {
console.error(colors.error('Failed to parse server message'));
}
});
ws.on('close', () => {
if (!callEnded && opened) {
if (transcriptLines.length > 0) {
transcriptLines.push('[Call interrupted — connection lost]');
flushTranscript();
}
console.log(colors.warning('Connection closed'));
if (callId) {
safeReject(new Error('Lost connection to call server before call ended.'));
return;
}
}
safeResolve();
});
ws.on('error', (err) => {
if (!opened) {
safeReject(err instanceof Error ? err : new Error(String(err)));
} else {
console.error(colors.error(`Connection error: ${err instanceof Error ? err.message : String(err)}`));
}
});
const onShutdown = () => {
console.log('');
if (transcriptLines.length > 0 && !callEnded) {
transcriptLines.push('[Call interrupted]');
flushTranscript();
}
if (callId && !callEnded && ws.readyState === WebSocket.OPEN) {
console.log(colors.info('Hanging up...'));
ws.send(JSON.stringify({ type: 'hangup', callId } satisfies ClientMessage));
setTimeout(() => safeResolve(), 2000);
} else {
safeResolve();
}
};
process.on('SIGINT', onShutdown);
process.on('SIGTERM', onShutdown);
});
}
export function callCommand(program: Command, getContext: () => CliContext): void {
program
.command('call')
.description('Make a phone call with AI voice')
.argument('<phone>', 'Phone number to call (E.164 format preferred, e.g., +1-555-123-4567)')
.requiredOption('-g, --goal <goal>', 'Goal for the call (e.g., "Book a hotel room for Feb 15")')
.requiredOption('-n, --name <name>', 'Customer name (e.g., "John Smith")')
.requiredOption('-e, --email <email>', 'Customer email for confirmations')
.requiredOption('--customer-phone <customerPhone>', 'Customer phone number for callbacks')
.option('-c, --context <context>', 'Additional context (e.g., "2 nights, king bed preferred")')
.option('-p, --port <port>', 'Server port')
.option('--interactive', 'Interactive mode - type responses manually', false)
.option('-o, --output-dir <dir>', 'Directory for call logs, transcripts, and recordings')
.option('--no-auto-infra', 'Do not auto-start ngrok + server when server is unavailable')
.action(async (phone: string, options: CallOptions) => {
const ctx = getContext();
await executeCall(phone, options, ctx);
});
program
.command('direct-booking')
.description('Book directly and negotiate a discount (uses customer info from config)')
.argument('<phone>', 'Hotel phone number to call (E.164 format preferred)')
.requiredOption('--hotel <name>', 'Hotel name (e.g., "Trisara Resort")')
.requiredOption('-i, --check-in <date>', 'Check-in date (YYYY-MM-DD)')
.requiredOption('-o, --check-out <date>', 'Check-out date (YYYY-MM-DD)')
.option('-r, --room <room>', 'Room type (default: cheapest available room)')
.option('--booking-price <amount>', 'Reference price from Booking.com (numeric)')
.option('--currency <code>', 'Currency code for reference price (default: THB)', 'THB')
.option('--discount <percent>', 'Discount percent to request (default: 10)', '10')
.option('-c, --context <context>', 'Additional context to pass to the assistant')
.option('-p, --port <port>', 'Server port')
.option('--interactive', 'Interactive mode - type responses manually', false)
.option('--output-dir <dir>', 'Directory for call logs, transcripts, and recordings')
.option('--no-auto-infra', 'Do not auto-start ngrok + server when server is unavailable')
.action(async (phone: string, options: DirectBookingOptions) => {
const ctx = getContext();
const { colors } = ctx;
const config = loadConfig();
if (!config.customerName || !config.customerEmail || !config.customerPhone) {
console.log(colors.error('Missing customer defaults in config.'));
console.log(colors.muted('Set: customerName, customerEmail, customerPhone'));
process.exit(1);
}
if (!/^\d{4}-\d{2}-\d{2}$/.test(options.checkIn) || !/^\d{4}-\d{2}-\d{2}$/.test(options.checkOut)) {
console.log(colors.error('Dates must be in YYYY-MM-DD format.'));
process.exit(1);
}
const discountValue = Number.parseFloat(options.discount ?? '10');
if (Number.isNaN(discountValue) || discountValue <= 0 || discountValue >= 100) {
console.log(colors.error(`Invalid discount percent: ${options.discount}`));
process.exit(1);
}
const room = options.room?.trim() || 'cheapest available room';
const currency = (options.currency ?? 'THB').trim().toUpperCase();
const bookingPrice = options.bookingPrice ? Number.parseFloat(options.bookingPrice) : undefined;
if (options.bookingPrice && Number.isNaN(bookingPrice)) {
console.log(colors.error(`Invalid booking price: ${options.bookingPrice}`));
process.exit(1);
}
const goalParts = [
`Book a room directly at ${options.hotel}`,
`for ${formatDateForSpeech(options.checkIn)} to ${formatDateForSpeech(options.checkOut)}`,
bookingPrice
? `and request ${discountValue}% off the Booking.com rate`
: 'and request a direct-booking discount',
];
const goal = `${goalParts.join(' ')}.`;
const negotiation = bookingPrice
? `Reference rate: ${currency} ${bookingPrice.toFixed(0)} on Booking.com. Ask for a ${discountValue}% direct-booking discount.`
: `Ask for the best direct-booking rate and request a ${discountValue}% discount if possible.`;
const contextLines = [
`Hotel: ${options.hotel}`,
`Dates: ${formatDateForSpeech(options.checkIn)} to ${formatDateForSpeech(options.checkOut)}`,
`Room preference: ${room}`,
negotiation,
'If discount not possible, ask for value-adds (breakfast, resort credit, upgrade, airport transfer, flexible cancellation).',
'Confirm final total incl. taxes/fees, cancellation policy, and get a confirmation number.',
'Request email confirmation.',
];
if (options.context) {
contextLines.push(`Additional context: ${options.context}`);
}
const callOptions: CallOptions = {
goal,
name: config.customerName,
email: config.customerEmail,
customerPhone: config.customerPhone,
context: contextLines.join('\n'),
port: options.port,
outputDir: options.outputDir,
interactive: options.interactive,
autoInfra: options.autoInfra,
};
await executeCall(phone, callOptions, ctx);
});
}
import type { Command } from 'commander';
import type { CliContext } from '../cli/shared.js';
import { isAgentBrowserInstalled } from '../lib/browser/agent-browser-client.js';
import { checkAvailability } from '../lib/browser/booking-scraper.js';
import { formatAvailabilityResult, formatError, formatVerbose } from '../lib/output.js';
interface CheckAvailabilityOptions {
checkIn: string;
checkOut: string;
guests?: string;
rooms?: string;
screenshot?: string;
headed?: boolean;
}
/**
* Validate date format (YYYY-MM-DD)
*/
function isValidDateFormat(dateStr: string): boolean {
return /^\d{4}-\d{2}-\d{2}$/.test(dateStr);
}
/**
* Validate date is not in the past
*/
function isNotPastDate(dateStr: string): boolean {
const date = new Date(dateStr);
const today = new Date();
today.setHours(0, 0, 0, 0);
return date >= today;
}
/**
* Validate checkout is after checkin
*/
function isCheckoutAfterCheckin(checkIn: string, checkOut: string): boolean {
return new Date(checkOut) > new Date(checkIn);
}
export function checkAvailabilityCommand(program: Command, getContext: () => CliContext): void {
program
.command('check-availability')
.alias('ca')
.alias('availability')
.description('Check availability and prices on Booking.com')
.argument('<query>', 'Hotel name or Booking.com URL')
.requiredOption('-i, --check-in <date>', 'Check-in date (YYYY-MM-DD)')
.requiredOption('-o, --check-out <date>', 'Check-out date (YYYY-MM-DD)')
.option('-g, --guests <n>', 'Number of guests (default: 2)')
.option('-r, --rooms <n>', 'Number of rooms (default: 1)')
.option('-s, --screenshot <path>', 'Save screenshot of results')
.option('--headed', 'Show browser window (for debugging)')
.action(async (query: string, options: CheckAvailabilityOptions) => {
const ctx = getContext();
// Check if agent-browser is installed
if (!isAgentBrowserInstalled()) {
console.log(formatError('agent-browser CLI is not installed. Install it to use availability checking.', ctx));
process.exit(1);
}
// Validate dates
if (!isValidDateFormat(options.checkIn)) {
console.log(formatError('Check-in date must be in YYYY-MM-DD format', ctx));
process.exit(1);
}
if (!isValidDateFormat(options.checkOut)) {
console.log(formatError('Check-out date must be in YYYY-MM-DD format', ctx));
process.exit(1);
}
if (!isNotPastDate(options.checkIn)) {
console.log(formatError('Check-in date cannot be in the past', ctx));
process.exit(1);
}
if (!isCheckoutAfterCheckin(options.checkIn, options.checkOut)) {
console.log(formatError('Check-out date must be after check-in date', ctx));
process.exit(1);
}
// Parse options
const guests = options.guests ? parseInt(options.guests, 10) : 2;
const rooms = options.rooms ? parseInt(options.rooms, 10) : 1;
if (options.guests && (Number.isNaN(guests) || guests < 1)) {
console.log(formatError('Guests must be a positive number', ctx));
process.exit(1);
}
if (options.rooms && (Number.isNaN(rooms) || rooms < 1)) {
console.log(formatError('Rooms must be a positive number', ctx));
process.exit(1);
}
const verboseMsg = formatVerbose(
`Checking availability for: ${query} (${options.checkIn} to ${options.checkOut})`,
ctx,
);
if (verboseMsg) console.log(verboseMsg);
// Check availability
const result = await checkAvailability({
query,
checkIn: options.checkIn,
checkOut: options.checkOut,
guests,
rooms,
screenshot: options.screenshot,
headed: options.headed,
});
if (!result.success) {
console.log(formatError(result.error, ctx));
process.exit(1);
}
console.log(formatAvailabilityResult(result.data, ctx));
});
}
import type { Command } from 'commander';
import type { CliContext } from '../cli/shared.js';
import type { ConciergeConfig } from '../lib/concierge-client-types.js';
import { deleteConfigValue, getConfigPath, loadConfig, setConfigValue } from '../lib/config.js';
const VALID_KEYS: (keyof ConciergeConfig)[] = [
'googlePlacesApiKey',
'instagramSessionId',
'timeoutMs',
'customerName',
'customerEmail',
'customerPhone',
'twilioAccountSid',
'twilioAuthToken',
'twilioPhoneNumber',
'deepgramApiKey',
'elevenLabsApiKey',
'elevenLabsVoiceId',
'ngrokAuthToken',
'callServerPort',
'anthropicApiKey',
'callOutputDir',
];
export function configCommand(program: Command, getContext: () => CliContext): void {
const config = program.command('config').description('Manage configuration');
// Show config
config
.command('show')
.description('Show current configuration')
.action(() => {
const ctx = getContext();
const current = loadConfig();
if (ctx.json) {
console.log(JSON.stringify(current, null, 2));
return;
}
const { colors } = ctx;
console.log('');
console.log(colors.highlight('Configuration'));
console.log(colors.muted(`Path: ${getConfigPath()}`));
console.log('');
for (const key of VALID_KEYS) {
const value = current[key];
if (value !== undefined) {
// Mask sensitive values
const displayValue =
key.toLowerCase().includes('key') || key.toLowerCase().includes('session')
? maskValue(String(value))
: String(value);
console.log(` ${colors.primary(key)}: ${displayValue}`);
} else {
console.log(` ${colors.primary(key)}: ${colors.muted('(not set)')}`);
}
}
console.log('');
});
// Set config value
config
.command('set')
.description('Set a configuration value')
.argument('<key>', `Configuration key (${VALID_KEYS.join(', ')})`)
.argument('<value>', 'Value to set')
.action((key: string, value: string) => {
const ctx = getContext();
if (!VALID_KEYS.includes(key as keyof ConciergeConfig)) {
console.log(ctx.colors.error(`Invalid key: ${key}`));
console.log(ctx.colors.muted(`Valid keys: ${VALID_KEYS.join(', ')}`));
process.exit(1);
}
const typedKey = key as keyof ConciergeConfig;
// Type coercion for numeric values
let typedValue: string | number = value;
if (typedKey === 'timeoutMs' || typedKey === 'callServerPort') {
typedValue = Number.parseInt(value, 10);
if (Number.isNaN(typedValue)) {
console.log(ctx.colors.error(`Invalid number: ${value}`));
process.exit(1);
}
}
setConfigValue(typedKey, typedValue as ConciergeConfig[typeof typedKey]);
if (ctx.json) {
console.log(JSON.stringify({ success: true, key, value: typedValue }));
} else {
console.log(ctx.colors.success(`Set ${key}`));
}
});
// Unset config value
config
.command('unset')
.description('Remove a configuration value')
.argument('<key>', 'Configuration key to remove')
.action((key: string) => {
const ctx = getContext();
if (!VALID_KEYS.includes(key as keyof ConciergeConfig)) {
console.log(ctx.colors.error(`Invalid key: ${key}`));
console.log(ctx.colors.muted(`Valid keys: ${VALID_KEYS.join(', ')}`));
process.exit(1);
}
deleteConfigValue(key as keyof ConciergeConfig);
if (ctx.json) {
console.log(JSON.stringify({ success: true, key, deleted: true }));
} else {
console.log(ctx.colors.success(`Removed ${key}`));
}
});
// Get config path
config
.command('path')
.description('Show configuration file path')
.action(() => {
const ctx = getContext();
if (ctx.json) {
console.log(JSON.stringify({ path: getConfigPath() }));
} else {
console.log(getConfigPath());
}
});
}
function maskValue(value: string): string {
if (value.length <= 8) {
return '****';
}
return `${value.slice(0, 4)}****${value.slice(-4)}`;
}
import type { Command } from 'commander';
import type { CliContext } from '../cli/shared.js';
import { ConciergeClient } from '../lib/concierge-client.js';
import { formatDossier, formatDossierList, formatError, formatVerbose } from '../lib/output.js';
import { isValidUrl } from '../lib/utils/url-parser.js';
interface FindOptions {
html?: string;
limit?: string;
minRating?: string;
type?: string;
radiusM?: string;
enrich?: boolean;
}
export function findCommand(program: Command, getContext: () => CliContext): void {
program
.command('find')
.alias('find-contact')
.alias('fc')
.alias('search')
.alias('s')
.description('Find contact details for a listing URL or search for places by text query')
.argument('<query>', 'Listing URL or text search query')
.option('--html <file>', 'Path to saved HTML file (for URL lookups)')
.option('-l, --limit <n>', 'Maximum number of results for text search (default: 10, max: 20)')
.option('--min-rating <n>', 'Minimum rating filter (0-5)')
.option('--type <type>', 'Place type: lodging, hotel, resort_hotel (default: lodging)')
.option('--radius-m <meters>', 'Search radius in meters (for coordinate searches)')
.option('--enrich', 'Enrich multi-result searches with website scraping (slower)')
.action(async (query: string, options: FindOptions) => {
const ctx = getContext();
const client = new ConciergeClient();
// Auto-detect: URL vs text query
if (isValidUrl(query)) {
// --- URL path: delegate to existing findContacts ---
let html: string | undefined;
if (options.html) {
const fs = await import('node:fs');
try {
html = fs.readFileSync(options.html, 'utf-8');
const msg = formatVerbose(`Loaded HTML from ${options.html}`, ctx);
if (msg) console.log(msg);
} catch (_error) {
console.log(formatError(`Failed to read HTML file: ${options.html}`, ctx));
process.exit(1);
}
}
const verboseMsg = formatVerbose(`Searching for contacts for: ${query}`, ctx);
if (verboseMsg) console.log(verboseMsg);
const result = await client.findContacts(query, {
html,
verbose: ctx.verbose,
});
if (!result.success) {
console.log(formatError(result.error, ctx));
process.exit(1);
}
console.log(formatDossier(result.data, ctx));
} else {
// --- Text query path: use findByQuery ---
const limit = options.limit ? parseInt(options.limit, 10) : 10;
const minRating = options.minRating ? parseFloat(options.minRating) : undefined;
const radiusMeters = options.radiusM ? parseInt(options.radiusM, 10) : undefined;
// Validate options
if (options.limit && (Number.isNaN(limit) || limit < 1 || limit > 20)) {
console.log(formatError('Limit must be between 1 and 20', ctx));
process.exit(1);
}
if (options.minRating && (Number.isNaN(minRating!) || minRating! < 0 || minRating! > 5)) {
console.log(formatError('Minimum rating must be between 0 and 5', ctx));
process.exit(1);
}
if (options.radiusM && (Number.isNaN(radiusMeters!) || radiusMeters! < 1)) {
console.log(formatError('Radius must be a positive number', ctx));
process.exit(1);
}
const result = await client.findByQuery(query, {
limit,
minRating,
type: options.type,
radiusMeters,
enrich: options.enrich,
verbose: ctx.verbose,
});
if (!result.success) {
console.log(formatError(result.error, ctx));
process.exit(1);
}
const dossiers = result.data;
if (dossiers.length === 0) {
console.log(formatError('No results found matching your criteria', ctx));
process.exit(0);
}
// Single result: full dossier view; multi-result: compact list
if (dossiers.length === 1) {
console.log(formatDossier(dossiers[0], ctx));
} else {
console.log(formatDossierList(dossiers, ctx));
}
}
});
}
import type { Command } from 'commander';
import type { CliContext } from '../cli/shared.js';
import { FlightQueueClient } from '../lib/flightqueue-client.js';
import type { FlightQueueData } from '../lib/flightqueue-types.js';
import { formatError, formatVerbose } from '../lib/output.js';
function formatWaitTimeLevel(level: string): string {
switch (level) {
case 'fast':
return 'Fast';
case 'moderate':
return 'Moderate';
case 'busy':
return 'Busy';
case 'very_busy':
return 'Very Busy';
default:
return 'Unknown';
}
}
function formatFlightQueueData(data: FlightQueueData, ctx: CliContext): string {
if (ctx.json) {
return JSON.stringify(data, null, 2);
}
const { colors } = ctx;
const lines: string[] = [];
// Header
lines.push('');
lines.push(colors.highlight('═══════════════════════════════════════════════════════════'));
lines.push(colors.highlight(` AIRPORT WAIT TIMES: ${data.airport.code}`));
lines.push(colors.highlight('═══════════════════════════════════════════════════════════'));
lines.push('');
// Airport Info
lines.push(colors.primary('▸ Airport Information'));
lines.push(` ${colors.muted('Code:')} ${data.airport.code}`);
lines.push(` ${colors.muted('Name:')} ${data.airport.name}`);
if (data.airport.city) {
lines.push(` ${colors.muted('City:')} ${data.airport.city}`);
}
if (data.airport.country) {
lines.push(` ${colors.muted('Country:')} ${data.airport.country}`);
}
lines.push('');
// Wait Times
lines.push(colors.primary('▸ Current Wait Times'));
// Security
const securityColor =
data.security.level === 'fast'
? colors.success
: data.security.level === 'moderate'
? colors.warning
: colors.error;
const securityTime = data.security.minutes ? `${data.security.minutes} min` : 'N/A';
lines.push(
` ${colors.muted('Security:')} ${securityColor(formatWaitTimeLevel(data.security.level))} (${securityTime})`,
);
// Immigration
const immigrationColor =
data.immigration.level === 'fast'
? colors.success
: data.immigration.level === 'moderate'
? colors.warning
: colors.error;
const immigrationTime = data.immigration.minutes ? `${data.immigration.minutes} min` : 'N/A';
lines.push(
` ${colors.muted('Immigration:')} ${immigrationColor(formatWaitTimeLevel(data.immigration.level))} (${immigrationTime})`,
);
if (data.trafficScore !== null) {
lines.push(` ${colors.muted('Traffic Score:')} ${data.trafficScore}`);
}
lines.push('');
// Recommended Arrival
if (data.recommendedArrival.domestic || data.recommendedArrival.international) {
lines.push(colors.primary('▸ Recommended Arrival'));
if (data.recommendedArrival.domestic) {
lines.push(` ${colors.muted('Domestic:')} ${data.recommendedArrival.domestic} min before departure`);
}
if (data.recommendedArrival.international) {
lines.push(` ${colors.muted('International:')} ${data.recommendedArrival.international} min before departure`);
}
lines.push('');
}
// Footer
lines.push(colors.muted(`Source: ${data.url}`));
lines.push(colors.muted(`Fetched: ${data.fetchedAt}`));
lines.push('');
return lines.join('\n');
}
export function flightqueueCommand(program: Command, getContext: () => CliContext): void {
program
.command('flightqueue')
.alias('fq')
.alias('queue')
.description('Get airport wait times from FlightQueue.com')
.argument('<airport>', 'Airport code (e.g., HKT) or city name (e.g., Phuket)')
.action(async (airport: string) => {
const ctx = getContext();
const client = new FlightQueueClient({ verbose: ctx.verbose });
const verboseMsg = formatVerbose(`Looking up wait times for: ${airport}`, ctx);
if (verboseMsg) console.log(verboseMsg);
const result = await client.getWaitTimes(airport);
if (!result.success) {
console.log(formatError(result.error, ctx));
process.exit(1);
}
console.log(formatFlightQueueData(result.data, ctx));
});
}
/**
* Server command - Start/stop/status the call server
*/
import { existsSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
import { homedir } from 'node:os';
import { join } from 'node:path';
import type { Command } from 'commander';
import type { CliContext } from '../cli/shared.js';
import { type CallConfig, createCallServer, validateCallConfig } from '../lib/call/index.js';
import { loadConfig } from '../lib/config.js';
const PID_FILE = join(homedir(), '.config', 'concierge', 'server.pid');
export function serverCommand(program: Command, getContext: () => CliContext): void {
const server = program.command('server').description('Manage the voice call server');
// Start server
server
.command('start')
.description('Start the voice call server')
.option('-p, --port <port>', 'Server port')
.option('--public-url <url>', 'Public URL (ngrok URL)')
.action(async (options: { port?: string; publicUrl?: string }) => {
const ctx = getContext();
const { colors } = ctx;
const config = loadConfig();
const port = options.port ? Number.parseInt(options.port, 10) : (config.callServerPort ?? 3000);
// Validate port number
if (Number.isNaN(port) || port < 1 || port > 65535) {
console.log(colors.error(`Invalid port number: ${options.port}. Must be between 1 and 65535.`));
process.exit(1);
}
// Check if already running
if (existsSync(PID_FILE)) {
const pid = readFileSync(PID_FILE, 'utf-8').trim();
try {
process.kill(Number(pid), 0);
console.log(colors.warning(`Server already running (PID: ${pid})`));
console.log(colors.muted('Use "concierge server stop" to stop it first'));
return;
} catch {
// Process not running, clean up stale PID file
unlinkSync(PID_FILE);
}
}
// Validate config
const callConfig: Partial<CallConfig> = {
twilioAccountSid: config.twilioAccountSid,
twilioAuthToken: config.twilioAuthToken,
twilioPhoneNumber: config.twilioPhoneNumber,
deepgramApiKey: config.deepgramApiKey,
elevenLabsApiKey: config.elevenLabsApiKey,
elevenLabsVoiceId: config.elevenLabsVoiceId,
anthropicApiKey: config.anthropicApiKey,
serverPort: port,
publicUrl: options.publicUrl ?? '',
};
// Check for missing config
const missing: string[] = [];
if (!callConfig.twilioAccountSid) missing.push('twilioAccountSid');
if (!callConfig.twilioAuthToken) missing.push('twilioAuthToken');
if (!callConfig.twilioPhoneNumber) missing.push('twilioPhoneNumber');
if (!callConfig.deepgramApiKey) missing.push('deepgramApiKey');
if (!callConfig.elevenLabsApiKey) missing.push('elevenLabsApiKey');
if (!callConfig.elevenLabsVoiceId) missing.push('elevenLabsVoiceId');
if (!callConfig.anthropicApiKey) missing.push('anthropicApiKey');
if (!callConfig.publicUrl) missing.push('publicUrl (use --public-url or set ngrokAuthToken)');
if (missing.length > 0) {
console.log(colors.error('Missing required configuration:'));
for (const key of missing) {
console.log(colors.muted(` - ${key}`));
}
console.log('');
console.log(colors.info('Set them with:'));
console.log(colors.muted(' concierge config set <key> <value>'));
process.exit(1);
}
if (!validateCallConfig(callConfig)) {
console.log(colors.error('Invalid configuration'));
process.exit(1);
}
console.log(colors.info('Starting voice call server...'));
console.log(colors.muted(`Port: ${port}`));
console.log(colors.muted(`Public URL: ${callConfig.publicUrl}`));
try {
const callServer = createCallServer(callConfig, port, callConfig.publicUrl);
// Save PID
writeFileSync(PID_FILE, String(process.pid));
await callServer.start();
console.log('');
console.log(colors.success('Server started successfully!'));
console.log('');
console.log(colors.highlight('Endpoints:'));
console.log(colors.muted(` Health: http://localhost:${port}/health`));
console.log(colors.muted(` Status: http://localhost:${port}/status`));
console.log(colors.muted(` Control: ws://localhost:${port}/control`));
console.log('');
console.log(colors.info('Press Ctrl+C to stop'));
// Handle shutdown
const shutdown = async () => {
console.log('');
console.log(colors.info('Shutting down...'));
await callServer.stop();
if (existsSync(PID_FILE)) {
unlinkSync(PID_FILE);
}
process.exit(0);
};
process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);
// Keep process running
await new Promise(() => {});
} catch (err) {
if (existsSync(PID_FILE)) {
unlinkSync(PID_FILE);
}
console.log(colors.error(`Failed to start server: ${err instanceof Error ? err.message : 'Unknown error'}`));
process.exit(1);
}
});
// Stop server
server
.command('stop')
.description('Stop the voice call server')
.action(() => {
const ctx = getContext();
const { colors } = ctx;
if (!existsSync(PID_FILE)) {
console.log(colors.warning('Server is not running'));
return;
}
const pid = readFileSync(PID_FILE, 'utf-8').trim();
try {
process.kill(Number(pid), 'SIGTERM');
console.log(colors.success(`Server stopped (PID: ${pid})`));
} catch {
console.log(colors.warning('Server process not found, cleaning up'));
}
unlinkSync(PID_FILE);
});
// Server status
server
.command('status')
.description('Check server status')
.action(async () => {
const ctx = getContext();
const { colors } = ctx;
const config = loadConfig();
const port = config.callServerPort ?? 3000;
if (!existsSync(PID_FILE)) {
if (ctx.json) {
console.log(JSON.stringify({ running: false }));
} else {
console.log(colors.warning('Server is not running'));
}
return;
}
const pid = readFileSync(PID_FILE, 'utf-8').trim();
// Check if process is alive
let processRunning = false;
try {
process.kill(Number(pid), 0);
processRunning = true;
} catch {
// Process not running
}
if (!processRunning) {
if (ctx.json) {
console.log(JSON.stringify({ running: false, stalePid: pid }));
} else {
console.log(colors.warning('Server process not found (stale PID file)'));
console.log(colors.muted('Run "concierge server stop" to clean up'));
}
return;
}
// Try to get status from server
try {
const response = await fetch(`http://localhost:${port}/status`);
const status = (await response.json()) as {
activeCalls: number;
controlClients: number;
publicUrl: string;
};
if (ctx.json) {
console.log(JSON.stringify({ running: true, pid, ...status }));
} else {
console.log(colors.success('Server is running'));
console.log(colors.muted(` PID: ${pid}`));
console.log(colors.muted(` Port: ${port}`));
console.log(colors.muted(` Active calls: ${status.activeCalls}`));
console.log(colors.muted(` Control clients: ${status.controlClients}`));
console.log(colors.muted(` Public URL: ${status.publicUrl}`));
}
} catch {
if (ctx.json) {
console.log(JSON.stringify({ running: true, pid, reachable: false }));
} else {
console.log(colors.success(`Server process running (PID: ${pid})`));
console.log(colors.warning(' Unable to reach HTTP endpoint'));
}
}
});
}
// Library exports
export type { FindByQueryOptions, FindContactOptions } from './lib/concierge-client.js';
export { ConciergeClient } from './lib/concierge-client.js';
export type {
ConciergeConfig,
ContactDossier,
ContactInfo,
Platform,
PropertyInfo,
Result,
SourceInfo,
} from './lib/concierge-client-types.js';