mirror of
https://github.com/Pavelevich/claw-alexa.git
synced 2026-03-11 21:54:15 +01:00
Initial commit: OpenClaw Alexa Skill
This commit is contained in:
10
.env.example
Normal file
10
.env.example
Normal file
@@ -0,0 +1,10 @@
|
||||
# OpenClaw Gateway Configuration
|
||||
OPENCLAW_GATEWAY_URL=https://your-machine.tailnet.ts.net
|
||||
OPENCLAW_GATEWAY_PASSWORD=your-gateway-password
|
||||
|
||||
# Optional: Fallback to Grok API if gateway is unavailable
|
||||
XAI_API_KEY=your-xai-api-key
|
||||
|
||||
# AWS Configuration (for deployment)
|
||||
AWS_REGION=us-east-1
|
||||
AWS_LAMBDA_FUNCTION_NAME=openclaw-alexa
|
||||
38
.gitignore
vendored
Normal file
38
.gitignore
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
package-lock.json
|
||||
|
||||
# Environment and secrets
|
||||
.env
|
||||
.env.local
|
||||
.env.production
|
||||
env.json
|
||||
*.pem
|
||||
*.key
|
||||
|
||||
# Build artifacts
|
||||
lambda.zip
|
||||
dist/
|
||||
.next/
|
||||
out/
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# Vercel
|
||||
.vercel/
|
||||
|
||||
# Local testing
|
||||
test-*.json
|
||||
*.zip
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 OpenClaw
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
330
README.md
Normal file
330
README.md
Normal file
@@ -0,0 +1,330 @@
|
||||
<p align="center">
|
||||
<img src="logoalexa.png" alt="OpenClaw Alexa" width="400"/>
|
||||
</p>
|
||||
|
||||
<h1 align="center">OpenClaw Alexa Skill</h1>
|
||||
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/badge/Alexa-00CAFF?style=for-the-badge&logo=amazon-alexa&logoColor=white" alt="Alexa"/>
|
||||
<img src="https://img.shields.io/badge/AWS_Lambda-FF9900?style=for-the-badge&logo=aws-lambda&logoColor=white" alt="Lambda"/>
|
||||
<img src="https://img.shields.io/badge/Tailscale-000000?style=for-the-badge&logo=tailscale&logoColor=white" alt="Tailscale"/>
|
||||
<img src="https://img.shields.io/badge/Node.js-339933?style=for-the-badge&logo=node.js&logoColor=white" alt="Node.js"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<strong>Voice-control your personal AI assistant through Amazon Alexa</strong>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="#features">Features</a> •
|
||||
<a href="#architecture">Architecture</a> •
|
||||
<a href="#quick-start">Quick Start</a> •
|
||||
<a href="#usage">Usage</a> •
|
||||
<a href="#troubleshooting">Troubleshooting</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License"/>
|
||||
<img src="https://img.shields.io/badge/node-%3E%3D18.0.0-brightgreen.svg" alt="Node Version"/>
|
||||
<img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg" alt="PRs Welcome"/>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
Connect your [OpenClaw](https://openclaw.ai) gateway to Alexa and interact with your personal AI using voice commands. Ask questions, read files from your workspace, execute commands, and more — all hands-free.
|
||||
|
||||
## Features
|
||||
|
||||
| Feature | Description |
|
||||
|---------|-------------|
|
||||
| **Voice-Activated AI** | Talk to your OpenClaw assistant through any Alexa device |
|
||||
| **Local File Access** | Read and interact with files on your computer |
|
||||
| **Secure Tunneling** | Uses Tailscale Funnel for encrypted, stable connections |
|
||||
| **Personal & Private** | Each user connects to their own OpenClaw instance |
|
||||
| **Fallback Support** | Optional Grok API fallback when gateway is unavailable |
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ OpenClaw Alexa Architecture │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
||||
│ │ │ │ │ │
|
||||
│ Alexa │ ─────▶ │ AWS Lambda │ ─────▶ │ Tailscale │
|
||||
│ Device │ ◀───── │ │ ◀───── │ Funnel │
|
||||
│ │ │ │ │ │
|
||||
└──────────────┘ └──────────────┘ └──────────────┘
|
||||
│ │
|
||||
│ │
|
||||
▼ ▼
|
||||
┌──────────────┐ ┌──────────────┐
|
||||
│ "Alexa, │ │ OpenClaw │
|
||||
│ ask claw │ │ Gateway │
|
||||
│ bot..." │ │ (Local) │
|
||||
└──────────────┘ └──────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────┐
|
||||
│ Your Files │
|
||||
│ & Commands │
|
||||
└──────────────┘
|
||||
```
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before you begin, ensure you have:
|
||||
|
||||
- [OpenClaw](https://openclaw.ai) installed and configured
|
||||
- [Tailscale](https://tailscale.com) account with Funnel enabled
|
||||
- [AWS Account](https://aws.amazon.com) for Lambda deployment
|
||||
- [Alexa Developer Account](https://developer.amazon.com/alexa)
|
||||
- Node.js 18+ (Lambda) / Node.js 22+ (OpenClaw)
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Step 1: Configure OpenClaw Gateway
|
||||
|
||||
Add the following to your `~/.openclaw/openclaw.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"gateway": {
|
||||
"port": 18789,
|
||||
"auth": {
|
||||
"mode": "password",
|
||||
"password": "your-secure-password"
|
||||
},
|
||||
"tailscale": {
|
||||
"mode": "funnel"
|
||||
},
|
||||
"http": {
|
||||
"endpoints": {
|
||||
"chatCompletions": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 2: Start OpenClaw Gateway
|
||||
|
||||
```bash
|
||||
openclaw gateway --force
|
||||
```
|
||||
|
||||
Note your Tailscale Funnel URL from the output:
|
||||
```
|
||||
[tailscale] funnel enabled: https://your-machine.tailnet.ts.net/
|
||||
```
|
||||
|
||||
### Step 3: Deploy Lambda Function
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/Pavelevich/openclaw-alexa.git
|
||||
cd openclaw-alexa
|
||||
|
||||
# Set environment variables
|
||||
export OPENCLAW_GATEWAY_URL="https://your-machine.tailnet.ts.net"
|
||||
export OPENCLAW_GATEWAY_PASSWORD="your-secure-password"
|
||||
|
||||
# Create deployment package
|
||||
cd lambda
|
||||
zip -r ../lambda.zip .
|
||||
|
||||
# Deploy to AWS Lambda
|
||||
aws lambda update-function-code \
|
||||
--function-name openclaw-alexa \
|
||||
--zip-file fileb://../lambda.zip \
|
||||
--region us-east-1
|
||||
|
||||
# Configure environment
|
||||
aws lambda update-function-configuration \
|
||||
--function-name openclaw-alexa \
|
||||
--environment "Variables={OPENCLAW_GATEWAY_URL=$OPENCLAW_GATEWAY_URL,OPENCLAW_GATEWAY_PASSWORD=$OPENCLAW_GATEWAY_PASSWORD}" \
|
||||
--region us-east-1
|
||||
```
|
||||
|
||||
### Step 4: Create Alexa Skill
|
||||
|
||||
1. Navigate to [Alexa Developer Console](https://developer.amazon.com/alexa/console/ask)
|
||||
2. Click **Create Skill** → Choose **Custom** model
|
||||
3. Set invocation name: `claw bot`
|
||||
4. Go to **JSON Editor** and paste contents of `interactionModels/custom/en-US.json`
|
||||
5. Click **Build Model**
|
||||
6. Under **Endpoint**, select **AWS Lambda ARN** and paste your function ARN
|
||||
7. **Save** and **Build** the skill
|
||||
|
||||
## Usage
|
||||
|
||||
### Voice Commands
|
||||
|
||||
| Command | What it does |
|
||||
|---------|--------------|
|
||||
| `"Alexa, open claw bot"` | Start a conversation session |
|
||||
| `"Alexa, ask claw bot tell me a joke"` | Quick one-shot question |
|
||||
| `"Alexa, ask claw bot what files are in my workspace"` | List your workspace files |
|
||||
| `"Alexa, ask claw bot read my notes file"` | Read file contents aloud |
|
||||
| `"Alexa, ask claw bot summarize the readme"` | Get document summaries |
|
||||
|
||||
### Example Interactions
|
||||
|
||||
```
|
||||
You: "Alexa, ask claw bot what's the weather like"
|
||||
Alexa: "I don't have direct weather access, but I can help you
|
||||
with files and tasks on your computer..."
|
||||
|
||||
You: "Alexa, ask claw bot list files in my documents"
|
||||
Alexa: "I found 5 files in your documents folder: notes.txt,
|
||||
project-plan.md, budget.xlsx..."
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
openclaw-alexa/
|
||||
├── lambda/
|
||||
│ ├── index.js # Lambda handler with OpenAI-compatible API
|
||||
│ └── package.json # Lambda dependencies
|
||||
├── interactionModels/
|
||||
│ └── custom/
|
||||
│ └── en-US.json # Alexa interaction model (38 utterances)
|
||||
├── skill.json # Alexa skill manifest
|
||||
├── .env.example # Environment variables template
|
||||
├── .gitignore
|
||||
├── LICENSE
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Description | Required |
|
||||
|----------|-------------|:--------:|
|
||||
| `OPENCLAW_GATEWAY_URL` | Your Tailscale Funnel URL | ✅ |
|
||||
| `OPENCLAW_GATEWAY_PASSWORD` | Gateway authentication password | ✅ |
|
||||
| `XAI_API_KEY` | Grok API key for fallback | ❌ |
|
||||
|
||||
### Lambda Settings
|
||||
|
||||
| Setting | Recommended Value |
|
||||
|---------|------------------|
|
||||
| Runtime | Node.js 18.x |
|
||||
| Memory | 256 MB |
|
||||
| Timeout | 30 seconds |
|
||||
| Region | us-east-1 (required for Alexa) |
|
||||
|
||||
## Security
|
||||
|
||||
| Layer | Protection |
|
||||
|-------|------------|
|
||||
| **Transport** | HTTPS via Tailscale Funnel (end-to-end encryption) |
|
||||
| **Authentication** | Password-based Bearer token authentication |
|
||||
| **Infrastructure** | No shared servers — each user runs their own instance |
|
||||
| **Credentials** | Environment variables only — never committed to code |
|
||||
|
||||
> **Important**: Never commit credentials or API keys. Use `.env` files locally and Lambda environment variables in production.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
<details>
|
||||
<summary><strong>"I'm not quite sure how to help you with that"</strong></summary>
|
||||
|
||||
The utterance didn't match a known pattern. Try rephrasing:
|
||||
- ✅ "tell me about {topic}"
|
||||
- ✅ "what is {query}"
|
||||
- ✅ "show me {something}"
|
||||
- ❌ Single-word queries
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Gateway timeout errors</strong></summary>
|
||||
|
||||
1. Verify OpenClaw gateway is running:
|
||||
```bash
|
||||
openclaw gateway --force
|
||||
```
|
||||
|
||||
2. Check Tailscale Funnel status:
|
||||
```bash
|
||||
tailscale funnel status
|
||||
```
|
||||
|
||||
3. Test the endpoint directly:
|
||||
```bash
|
||||
curl https://your-machine.tailnet.ts.net/health
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>401 Unauthorized</strong></summary>
|
||||
|
||||
- Verify `OPENCLAW_GATEWAY_PASSWORD` matches your `openclaw.json` config
|
||||
- Ensure auth mode is set to `password` (not `token`)
|
||||
- Check the password doesn't have special characters that need escaping
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Skill not responding on phone</strong></summary>
|
||||
|
||||
- Ensure skill is enabled in the Alexa app
|
||||
- Check you're using the same Amazon account as the developer console
|
||||
- Try: Alexa app → Skills → Your Skills → Dev → Enable
|
||||
|
||||
</details>
|
||||
|
||||
## API Reference
|
||||
|
||||
The Lambda uses the OpenAI-compatible chat completions endpoint:
|
||||
|
||||
```
|
||||
POST /v1/chat/completions
|
||||
Authorization: Bearer {password}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"model": "openclaw",
|
||||
"messages": [
|
||||
{"role": "system", "content": "..."},
|
||||
{"role": "user", "content": "user query"}
|
||||
],
|
||||
"stream": false
|
||||
}
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Please feel free to submit a Pull Request.
|
||||
|
||||
1. Fork the repository
|
||||
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
||||
3. Commit your changes (`git commit -m 'Add amazing feature'`)
|
||||
4. Push to the branch (`git push origin feature/amazing-feature`)
|
||||
5. Open a Pull Request
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||
|
||||
## Links
|
||||
|
||||
- [OpenClaw Documentation](https://docs.openclaw.ai)
|
||||
- [Tailscale Funnel Guide](https://tailscale.com/kb/1223/funnel/)
|
||||
- [Alexa Skills Kit Documentation](https://developer.amazon.com/en-US/alexa/alexa-skills-kit)
|
||||
- [AWS Lambda Developer Guide](https://docs.aws.amazon.com/lambda/)
|
||||
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
Built with <a href="https://openclaw.ai">OpenClaw</a>
|
||||
</p>
|
||||
147
deploy.sh
Executable file
147
deploy.sh
Executable file
@@ -0,0 +1,147 @@
|
||||
#!/bin/bash
|
||||
# OpenClaw Alexa Skill - AWS Lambda Deployment Script
|
||||
# Run: ./deploy.sh
|
||||
|
||||
set -e
|
||||
|
||||
FUNCTION_NAME="openclaw-alexa"
|
||||
REGION="${AWS_REGION:-us-east-1}"
|
||||
RUNTIME="nodejs20.x"
|
||||
TIMEOUT=30
|
||||
MEMORY=128
|
||||
|
||||
# Colors
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
RED='\033[0;31m'
|
||||
NC='\033[0m'
|
||||
|
||||
echo -e "${GREEN}═══════════════════════════════════════${NC}"
|
||||
echo -e "${GREEN} OpenClaw Alexa - Lambda Deployment ${NC}"
|
||||
echo -e "${GREEN}═══════════════════════════════════════${NC}"
|
||||
|
||||
# Check AWS CLI
|
||||
if ! command -v aws &> /dev/null; then
|
||||
echo -e "${RED}Error: AWS CLI not installed${NC}"
|
||||
echo "Install: brew install awscli"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check AWS credentials
|
||||
if ! aws sts get-caller-identity &> /dev/null; then
|
||||
echo -e "${RED}Error: AWS not configured${NC}"
|
||||
echo "Run: aws configure"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
|
||||
echo -e "${GREEN}AWS Account:${NC} $ACCOUNT_ID"
|
||||
echo -e "${GREEN}Region:${NC} $REGION"
|
||||
|
||||
# Create deployment package
|
||||
echo -e "\n${YELLOW}Creating deployment package...${NC}"
|
||||
cd "$(dirname "$0")/lambda"
|
||||
zip -r ../lambda.zip index.js
|
||||
|
||||
cd ..
|
||||
|
||||
# Check if function exists
|
||||
if aws lambda get-function --function-name $FUNCTION_NAME --region $REGION &> /dev/null; then
|
||||
echo -e "${YELLOW}Updating existing function...${NC}"
|
||||
aws lambda update-function-code \
|
||||
--function-name $FUNCTION_NAME \
|
||||
--zip-file fileb://lambda.zip \
|
||||
--region $REGION
|
||||
else
|
||||
echo -e "${YELLOW}Creating new Lambda function...${NC}"
|
||||
|
||||
# Create execution role if needed
|
||||
ROLE_NAME="openclaw-alexa-role"
|
||||
ROLE_ARN="arn:aws:iam::${ACCOUNT_ID}:role/${ROLE_NAME}"
|
||||
|
||||
if ! aws iam get-role --role-name $ROLE_NAME &> /dev/null; then
|
||||
echo -e "${YELLOW}Creating IAM role...${NC}"
|
||||
|
||||
cat > trust-policy.json << 'EOF'
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Principal": {
|
||||
"Service": "lambda.amazonaws.com"
|
||||
},
|
||||
"Action": "sts:AssumeRole"
|
||||
}
|
||||
]
|
||||
}
|
||||
EOF
|
||||
|
||||
aws iam create-role \
|
||||
--role-name $ROLE_NAME \
|
||||
--assume-role-policy-document file://trust-policy.json
|
||||
|
||||
aws iam attach-role-policy \
|
||||
--role-name $ROLE_NAME \
|
||||
--policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
|
||||
|
||||
echo "Waiting for role to propagate..."
|
||||
sleep 10
|
||||
|
||||
rm trust-policy.json
|
||||
fi
|
||||
|
||||
# Create function
|
||||
aws lambda create-function \
|
||||
--function-name $FUNCTION_NAME \
|
||||
--runtime $RUNTIME \
|
||||
--role $ROLE_ARN \
|
||||
--handler index.handler \
|
||||
--zip-file fileb://lambda.zip \
|
||||
--timeout $TIMEOUT \
|
||||
--memory-size $MEMORY \
|
||||
--region $REGION
|
||||
fi
|
||||
|
||||
# Set environment variables (use your own values)
|
||||
echo -e "${YELLOW}Setting environment variables...${NC}"
|
||||
if [ -z "$OPENCLAW_GATEWAY_URL" ] || [ -z "$OPENCLAW_GATEWAY_PASSWORD" ]; then
|
||||
echo -e "${RED}Error: Set OPENCLAW_GATEWAY_URL and OPENCLAW_GATEWAY_PASSWORD first${NC}"
|
||||
echo "export OPENCLAW_GATEWAY_URL=https://your-machine.tailnet.ts.net"
|
||||
echo "export OPENCLAW_GATEWAY_PASSWORD=your-password"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
aws lambda update-function-configuration \
|
||||
--function-name $FUNCTION_NAME \
|
||||
--environment "Variables={OPENCLAW_GATEWAY_URL=$OPENCLAW_GATEWAY_URL,OPENCLAW_GATEWAY_PASSWORD=$OPENCLAW_GATEWAY_PASSWORD}" \
|
||||
--region $REGION
|
||||
|
||||
# Get function ARN
|
||||
LAMBDA_ARN=$(aws lambda get-function --function-name $FUNCTION_NAME --region $REGION --query 'Configuration.FunctionArn' --output text)
|
||||
|
||||
# Clean up
|
||||
rm -f lambda.zip
|
||||
|
||||
echo -e "\n${GREEN}═══════════════════════════════════════${NC}"
|
||||
echo -e "${GREEN} Deployment Complete! ${NC}"
|
||||
echo -e "${GREEN}═══════════════════════════════════════${NC}"
|
||||
echo -e "\nLambda ARN: ${YELLOW}$LAMBDA_ARN${NC}"
|
||||
echo -e "\n${GREEN}Next Steps:${NC}"
|
||||
echo "1. Go to: https://developer.amazon.com/alexa/console/ask"
|
||||
echo "2. Create New Skill → Name: 'OpenClaw' → Custom → Provision your own"
|
||||
echo "3. Copy the Skill ID"
|
||||
echo "4. Add Alexa trigger to Lambda:"
|
||||
echo " aws lambda add-permission \\"
|
||||
echo " --function-name $FUNCTION_NAME \\"
|
||||
echo " --statement-id alexa-skill \\"
|
||||
echo " --action lambda:InvokeFunction \\"
|
||||
echo " --principal alexa-appkit.amazon.com \\"
|
||||
echo " --event-source-token <YOUR_SKILL_ID>"
|
||||
echo ""
|
||||
echo "5. In Alexa Console → JSON Editor → paste contents of:"
|
||||
echo " interactionModels/custom/en-US.json"
|
||||
echo ""
|
||||
echo "6. Endpoint → AWS Lambda ARN → paste: $LAMBDA_ARN"
|
||||
echo "7. Save & Build Model"
|
||||
echo "8. Test tab → say 'ask claw bot to tell me a joke'"
|
||||
93
interactionModels/custom/en-US.json
Normal file
93
interactionModels/custom/en-US.json
Normal file
@@ -0,0 +1,93 @@
|
||||
{
|
||||
"interactionModel": {
|
||||
"languageModel": {
|
||||
"invocationName": "claw bot",
|
||||
"intents": [
|
||||
{
|
||||
"name": "ChatIntent",
|
||||
"slots": [
|
||||
{
|
||||
"name": "query",
|
||||
"type": "AMAZON.SearchQuery"
|
||||
}
|
||||
],
|
||||
"samples": [
|
||||
"I need {query}",
|
||||
"ask about {query}",
|
||||
"tell me about {query}",
|
||||
"tell me {query}",
|
||||
"can you {query}",
|
||||
"please {query}",
|
||||
"I want to know {query}",
|
||||
"what is {query}",
|
||||
"what are {query}",
|
||||
"what about {query}",
|
||||
"how do I {query}",
|
||||
"how to {query}",
|
||||
"help me with {query}",
|
||||
"explain {query}",
|
||||
"describe {query}",
|
||||
"do you know {query}",
|
||||
"I have a question about {query}",
|
||||
"question about {query}",
|
||||
"say {query}",
|
||||
"respond to {query}",
|
||||
"answer {query}",
|
||||
"I want {query}",
|
||||
"give me {query}",
|
||||
"show me {query}",
|
||||
"find {query}",
|
||||
"search for {query}",
|
||||
"look up {query}",
|
||||
"who is {query}",
|
||||
"who are {query}",
|
||||
"where is {query}",
|
||||
"when is {query}",
|
||||
"why is {query}",
|
||||
"which {query}",
|
||||
"could you {query}",
|
||||
"would you {query}",
|
||||
"will you {query}",
|
||||
"let me know {query}",
|
||||
"I wonder {query}",
|
||||
"talk to me about {query}"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "AMAZON.HelpIntent",
|
||||
"samples": []
|
||||
},
|
||||
{
|
||||
"name": "AMAZON.StopIntent",
|
||||
"samples": []
|
||||
},
|
||||
{
|
||||
"name": "AMAZON.CancelIntent",
|
||||
"samples": []
|
||||
},
|
||||
{
|
||||
"name": "AMAZON.FallbackIntent",
|
||||
"samples": []
|
||||
}
|
||||
],
|
||||
"types": []
|
||||
},
|
||||
"dialog": {
|
||||
"intents": [
|
||||
{
|
||||
"name": "ChatIntent",
|
||||
"confirmationRequired": false,
|
||||
"prompts": {},
|
||||
"slots": [
|
||||
{
|
||||
"name": "query",
|
||||
"type": "AMAZON.SearchQuery",
|
||||
"confirmationRequired": false,
|
||||
"elicitationRequired": false
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
245
lambda/index.js
Normal file
245
lambda/index.js
Normal file
@@ -0,0 +1,245 @@
|
||||
/**
|
||||
* Alexa Skill Lambda - OpenClaw/Grok Integration
|
||||
* Connects Alexa to your personal AI gateway
|
||||
*/
|
||||
|
||||
const https = require('https');
|
||||
const http = require('http');
|
||||
|
||||
// Gateway configuration - set via environment variables or Account Linking
|
||||
const DEFAULT_GATEWAY_URL = process.env.OPENCLAW_GATEWAY_URL || '';
|
||||
const DEFAULT_GATEWAY_PASSWORD = process.env.OPENCLAW_GATEWAY_PASSWORD || '';
|
||||
|
||||
// Fallback to Grok API
|
||||
const XAI_API_KEY = process.env.XAI_API_KEY || '';
|
||||
|
||||
// Call OpenClaw gateway using OpenAI-compatible endpoint
|
||||
async function callGateway(message) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = new URL(DEFAULT_GATEWAY_URL);
|
||||
|
||||
// OpenClaw uses OpenAI-compatible chat completions endpoint
|
||||
const postData = JSON.stringify({
|
||||
model: 'openclaw',
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content: 'You are Smart Claw, a helpful AI assistant connected via Alexa. Keep responses concise and suitable for voice output (under 150 words).'
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: message
|
||||
}
|
||||
],
|
||||
stream: false
|
||||
});
|
||||
|
||||
const options = {
|
||||
hostname: url.hostname,
|
||||
port: 443,
|
||||
path: '/v1/chat/completions',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': Buffer.byteLength(postData),
|
||||
'Authorization': `Bearer ${DEFAULT_GATEWAY_PASSWORD}`
|
||||
},
|
||||
timeout: 25000
|
||||
};
|
||||
|
||||
console.log('Calling OpenClaw gateway:', DEFAULT_GATEWAY_URL + '/v1/chat/completions');
|
||||
|
||||
const req = https.request(options, (res) => {
|
||||
let data = '';
|
||||
res.on('data', chunk => data += chunk);
|
||||
res.on('end', () => {
|
||||
console.log('Gateway response status:', res.statusCode);
|
||||
console.log('Gateway response data:', data.substring(0, 500));
|
||||
|
||||
// If gateway returns HTML or error, fall back to Grok
|
||||
if (res.statusCode !== 200 || data.includes('<!DOCTYPE') || data.includes('<html')) {
|
||||
reject(new Error(`Gateway returned status ${res.statusCode}`));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const json = JSON.parse(data);
|
||||
// OpenAI-compatible response format
|
||||
if (json.choices && json.choices[0] && json.choices[0].message) {
|
||||
resolve(json.choices[0].message.content);
|
||||
} else if (json.error) {
|
||||
reject(new Error(json.error.message || 'Gateway error'));
|
||||
} else {
|
||||
reject(new Error('Unexpected gateway response format'));
|
||||
}
|
||||
} catch (e) {
|
||||
reject(new Error('Could not parse gateway response: ' + e.message));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (e) => {
|
||||
console.error('Gateway request error:', e.message);
|
||||
reject(e);
|
||||
});
|
||||
|
||||
req.on('timeout', () => {
|
||||
console.error('Gateway request timeout');
|
||||
req.destroy();
|
||||
reject(new Error('Gateway timeout'));
|
||||
});
|
||||
|
||||
req.write(postData);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
// Call Grok API directly (fallback)
|
||||
async function callGrok(message) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const postData = JSON.stringify({
|
||||
model: 'grok-3-fast',
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content: 'You are a helpful AI assistant called Smart Claw. Keep responses concise and suitable for voice output (under 150 words).'
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: message
|
||||
}
|
||||
],
|
||||
max_tokens: 400
|
||||
});
|
||||
|
||||
const options = {
|
||||
hostname: 'api.x.ai',
|
||||
port: 443,
|
||||
path: '/v1/chat/completions',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': Buffer.byteLength(postData),
|
||||
'Authorization': `Bearer ${XAI_API_KEY}`
|
||||
}
|
||||
};
|
||||
|
||||
const req = https.request(options, (res) => {
|
||||
let data = '';
|
||||
res.on('data', chunk => data += chunk);
|
||||
res.on('end', () => {
|
||||
try {
|
||||
const json = JSON.parse(data);
|
||||
if (json.choices && json.choices[0] && json.choices[0].message) {
|
||||
resolve(json.choices[0].message.content);
|
||||
} else {
|
||||
resolve('Sorry, I could not process that request.');
|
||||
}
|
||||
} catch (e) {
|
||||
resolve('Sorry, I encountered an error.');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', reject);
|
||||
req.write(postData);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
// Main handler
|
||||
exports.handler = async (event) => {
|
||||
console.log('Alexa Request:', JSON.stringify(event, null, 2));
|
||||
|
||||
const requestType = event.request.type;
|
||||
|
||||
// Launch request
|
||||
if (requestType === 'LaunchRequest') {
|
||||
return buildResponse(
|
||||
'Welcome to Smart Claw, connected to your OpenClaw gateway. What would you like to know?',
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
// Intent request
|
||||
if (requestType === 'IntentRequest') {
|
||||
const intentName = event.request.intent.name;
|
||||
|
||||
if (intentName === 'AMAZON.HelpIntent') {
|
||||
return buildResponse(
|
||||
'I am connected to your OpenClaw AI gateway. You can ask me anything. Try saying: scan my network, or tell me a joke.',
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
if (intentName === 'AMAZON.StopIntent' || intentName === 'AMAZON.CancelIntent') {
|
||||
return buildResponse('Goodbye!', true);
|
||||
}
|
||||
|
||||
if (intentName === 'AMAZON.FallbackIntent') {
|
||||
return buildResponse('I did not catch that. Please try again.', false);
|
||||
}
|
||||
|
||||
// Handle ChatIntent - main conversation
|
||||
if (intentName === 'ChatIntent') {
|
||||
const userMessage = event.request.intent.slots?.query?.value || 'Hello';
|
||||
console.log('User message:', userMessage);
|
||||
|
||||
try {
|
||||
let response;
|
||||
|
||||
// Try gateway first
|
||||
try {
|
||||
console.log('Attempting gateway call...');
|
||||
response = await callGateway(userMessage);
|
||||
console.log('Gateway success:', response?.substring(0, 100));
|
||||
} catch (gatewayError) {
|
||||
console.log('Gateway failed, using Grok:', gatewayError.message);
|
||||
// Fallback to Grok
|
||||
if (XAI_API_KEY) {
|
||||
response = await callGrok(userMessage);
|
||||
} else {
|
||||
response = 'Sorry, I could not connect to the gateway. Please check if OpenClaw is running.';
|
||||
}
|
||||
}
|
||||
|
||||
// Clean and truncate response
|
||||
let speechResponse = String(response).trim();
|
||||
if (speechResponse.length > 6000) {
|
||||
speechResponse = speechResponse.substring(0, 5900) + '... That is all for now.';
|
||||
}
|
||||
|
||||
return buildResponse(speechResponse, false);
|
||||
} catch (error) {
|
||||
console.error('Handler error:', error);
|
||||
return buildResponse('Sorry, I encountered an error. Please try again.', false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Session ended
|
||||
if (requestType === 'SessionEndedRequest') {
|
||||
return { version: '1.0', response: {} };
|
||||
}
|
||||
|
||||
return buildResponse('I did not understand that. Please try again.', false);
|
||||
};
|
||||
|
||||
function buildResponse(speechText, shouldEndSession) {
|
||||
return {
|
||||
version: '1.0',
|
||||
response: {
|
||||
outputSpeech: {
|
||||
type: 'PlainText',
|
||||
text: speechText
|
||||
},
|
||||
shouldEndSession: shouldEndSession,
|
||||
reprompt: shouldEndSession ? undefined : {
|
||||
outputSpeech: {
|
||||
type: 'PlainText',
|
||||
text: 'What else would you like to know?'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
9
lambda/package.json
Normal file
9
lambda/package.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "openclaw-alexa-lambda",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-dynamodb": "^3.400.0",
|
||||
"@aws-sdk/lib-dynamodb": "^3.400.0"
|
||||
}
|
||||
}
|
||||
BIN
logoalexa.png
Normal file
BIN
logoalexa.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 508 KiB |
40
skill.json
Normal file
40
skill.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"manifest": {
|
||||
"apis": {
|
||||
"custom": {
|
||||
"endpoint": {
|
||||
"sourceDir": "lambda",
|
||||
"uri": "arn:aws:lambda:us-east-1:YOUR_ACCOUNT_ID:function:openclaw-alexa"
|
||||
}
|
||||
}
|
||||
},
|
||||
"manifestVersion": "1.0",
|
||||
"publishingInformation": {
|
||||
"locales": {
|
||||
"en-US": {
|
||||
"name": "OpenClaw",
|
||||
"summary": "Your AI-powered personal assistant with Grok",
|
||||
"description": "OpenClaw is an AI assistant powered by Grok that can scan networks, check password security, control smart home devices, and answer any question.",
|
||||
"examplePhrases": [
|
||||
"Alexa, ask OpenClaw to scan my network",
|
||||
"Alexa, tell OpenClaw to check if my password is safe",
|
||||
"Alexa, ask OpenClaw what devices are online"
|
||||
],
|
||||
"keywords": [
|
||||
"AI",
|
||||
"assistant",
|
||||
"Grok",
|
||||
"security",
|
||||
"smart home"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"privacyAndCompliance": {
|
||||
"allowsPurchases": false,
|
||||
"isExportCompliant": true,
|
||||
"containsAds": false,
|
||||
"isChildDirected": false
|
||||
}
|
||||
}
|
||||
}
|
||||
5
webapp/.gitignore
vendored
Normal file
5
webapp/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
node_modules
|
||||
.next
|
||||
.env.local
|
||||
.env
|
||||
.vercel
|
||||
49
webapp/app/api/user/register/route.js
Normal file
49
webapp/app/api/user/register/route.js
Normal file
@@ -0,0 +1,49 @@
|
||||
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
|
||||
import { DynamoDBDocumentClient, PutCommand } from '@aws-sdk/lib-dynamodb';
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
export async function POST(request) {
|
||||
try {
|
||||
const { accessToken, gatewayUrl, gatewayToken } = await request.json();
|
||||
|
||||
console.log('Register request received:', { accessToken: accessToken?.substring(0, 10), gatewayUrl });
|
||||
|
||||
if (!accessToken || !gatewayUrl || !gatewayToken) {
|
||||
return NextResponse.json({ success: false, error: 'Missing fields' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Check if AWS credentials are available
|
||||
if (!process.env.AWS_ACCESS_KEY_ID || !process.env.AWS_SECRET_ACCESS_KEY) {
|
||||
console.error('AWS credentials not configured');
|
||||
return NextResponse.json({ success: false, error: 'Server configuration error' }, { status: 500 });
|
||||
}
|
||||
|
||||
const client = new DynamoDBClient({
|
||||
region: process.env.AWS_REGION || 'us-east-1',
|
||||
credentials: {
|
||||
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
|
||||
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
|
||||
}
|
||||
});
|
||||
|
||||
const docClient = DynamoDBDocumentClient.from(client);
|
||||
|
||||
// Store user config with accessToken as the key
|
||||
await docClient.send(new PutCommand({
|
||||
TableName: 'openclaw-users',
|
||||
Item: {
|
||||
userId: accessToken,
|
||||
gatewayUrl,
|
||||
gatewayToken,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString()
|
||||
}
|
||||
}));
|
||||
|
||||
console.log('User registered successfully');
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Registration error:', error.message, error.stack);
|
||||
return NextResponse.json({ success: false, error: error.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
40
webapp/app/api/user/test/route.js
Normal file
40
webapp/app/api/user/test/route.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
export async function POST(request) {
|
||||
try {
|
||||
const { gatewayUrl, gatewayToken } = await request.json();
|
||||
|
||||
if (!gatewayUrl) {
|
||||
return NextResponse.json({ success: false, error: 'Missing gateway URL' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Try to reach the gateway
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 5000);
|
||||
|
||||
try {
|
||||
const res = await fetch(gatewayUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${gatewayToken}`,
|
||||
'ngrok-skip-browser-warning': 'true'
|
||||
},
|
||||
signal: controller.signal
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
// If we get any response (even 401), the gateway is reachable
|
||||
return NextResponse.json({ success: true, status: res.status });
|
||||
} catch (fetchError) {
|
||||
clearTimeout(timeoutId);
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: fetchError.name === 'AbortError' ? 'Timeout' : fetchError.message
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Test error:', error);
|
||||
return NextResponse.json({ success: false, error: error.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
21
webapp/app/layout.js
Normal file
21
webapp/app/layout.js
Normal file
@@ -0,0 +1,21 @@
|
||||
export const metadata = {
|
||||
title: 'OpenClaw Alexa - Connect Your Gateway',
|
||||
description: 'Link your OpenClaw or Moltbot gateway to Alexa',
|
||||
}
|
||||
|
||||
export default function RootLayout({ children }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body style={{
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
||||
background: 'linear-gradient(135deg, #1a1a2e 0%, #16213e 100%)',
|
||||
minHeight: '100vh',
|
||||
color: '#fff'
|
||||
}}>
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
236
webapp/app/page.js
Normal file
236
webapp/app/page.js
Normal file
@@ -0,0 +1,236 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
export default function Home() {
|
||||
const [gatewayUrl, setGatewayUrl] = useState('');
|
||||
const [gatewayToken, setGatewayToken] = useState('');
|
||||
const [status, setStatus] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// Get state/redirect from URL params (for Alexa Account Linking)
|
||||
const searchParams = typeof window !== 'undefined'
|
||||
? new URLSearchParams(window.location.search)
|
||||
: null;
|
||||
const alexaState = searchParams?.get('state');
|
||||
const alexaRedirect = searchParams?.get('redirect_uri');
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
setStatus('');
|
||||
|
||||
try {
|
||||
// Generate a simple access token
|
||||
const accessToken = btoa(`${Date.now()}-${Math.random().toString(36).substr(2, 9)}`);
|
||||
|
||||
const res = await fetch('/api/user/register', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
accessToken,
|
||||
gatewayUrl,
|
||||
gatewayToken
|
||||
})
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (data.success) {
|
||||
setStatus('success');
|
||||
|
||||
// If this is Alexa Account Linking, redirect back
|
||||
if (alexaRedirect && alexaState) {
|
||||
const redirectUrl = `${alexaRedirect}#state=${alexaState}&access_token=${accessToken}&token_type=Bearer`;
|
||||
window.location.href = redirectUrl;
|
||||
}
|
||||
} else {
|
||||
setStatus('error');
|
||||
}
|
||||
} catch (err) {
|
||||
setStatus('error');
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const testConnection = async () => {
|
||||
setLoading(true);
|
||||
setStatus('testing');
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/user/test', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ gatewayUrl, gatewayToken })
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
setStatus(data.success ? 'connected' : 'failed');
|
||||
} catch {
|
||||
setStatus('failed');
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<main style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
minHeight: '100vh',
|
||||
padding: '20px'
|
||||
}}>
|
||||
<div style={{
|
||||
background: 'rgba(255,255,255,0.05)',
|
||||
borderRadius: '20px',
|
||||
padding: '40px',
|
||||
maxWidth: '500px',
|
||||
width: '100%',
|
||||
backdropFilter: 'blur(10px)',
|
||||
border: '1px solid rgba(255,255,255,0.1)'
|
||||
}}>
|
||||
<div style={{ textAlign: 'center', marginBottom: '30px' }}>
|
||||
<h1 style={{
|
||||
fontSize: '2.5rem',
|
||||
margin: 0,
|
||||
background: 'linear-gradient(90deg, #00d4ff, #7b2ff7)',
|
||||
WebkitBackgroundClip: 'text',
|
||||
WebkitTextFillColor: 'transparent'
|
||||
}}>
|
||||
OpenClaw + Alexa
|
||||
</h1>
|
||||
<p style={{ color: '#888', marginTop: '10px' }}>
|
||||
Connect your AI gateway to Alexa
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div style={{ marginBottom: '20px' }}>
|
||||
<label style={{ display: 'block', marginBottom: '8px', color: '#aaa' }}>
|
||||
Gateway URL
|
||||
</label>
|
||||
<input
|
||||
type="url"
|
||||
value={gatewayUrl}
|
||||
onChange={(e) => setGatewayUrl(e.target.value)}
|
||||
placeholder="https://xxxx.ngrok-free.app"
|
||||
required
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '15px',
|
||||
borderRadius: '10px',
|
||||
border: '1px solid rgba(255,255,255,0.2)',
|
||||
background: 'rgba(0,0,0,0.3)',
|
||||
color: '#fff',
|
||||
fontSize: '16px',
|
||||
boxSizing: 'border-box'
|
||||
}}
|
||||
/>
|
||||
<small style={{ color: '#666' }}>Your ngrok, Tailscale, or public URL</small>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '20px' }}>
|
||||
<label style={{ display: 'block', marginBottom: '8px', color: '#aaa' }}>
|
||||
Gateway Token
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={gatewayToken}
|
||||
onChange={(e) => setGatewayToken(e.target.value)}
|
||||
placeholder="Your gateway auth token"
|
||||
required
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '15px',
|
||||
borderRadius: '10px',
|
||||
border: '1px solid rgba(255,255,255,0.2)',
|
||||
background: 'rgba(0,0,0,0.3)',
|
||||
color: '#fff',
|
||||
fontSize: '16px',
|
||||
boxSizing: 'border-box'
|
||||
}}
|
||||
/>
|
||||
<small style={{ color: '#666' }}>Find in ~/.openclaw/openclaw.json</small>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: '10px' }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={testConnection}
|
||||
disabled={loading || !gatewayUrl}
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: '15px',
|
||||
borderRadius: '10px',
|
||||
border: 'none',
|
||||
background: 'rgba(255,255,255,0.1)',
|
||||
color: '#fff',
|
||||
fontSize: '16px',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
Test Connection
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
style={{
|
||||
flex: 2,
|
||||
padding: '15px',
|
||||
borderRadius: '10px',
|
||||
border: 'none',
|
||||
background: 'linear-gradient(90deg, #00d4ff, #7b2ff7)',
|
||||
color: '#fff',
|
||||
fontSize: '16px',
|
||||
fontWeight: 'bold',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
{loading ? 'Connecting...' : (alexaRedirect ? 'Link to Alexa' : 'Save Configuration')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{status && (
|
||||
<div style={{
|
||||
marginTop: '20px',
|
||||
padding: '15px',
|
||||
borderRadius: '10px',
|
||||
textAlign: 'center',
|
||||
background: status === 'success' || status === 'connected'
|
||||
? 'rgba(0,255,100,0.1)'
|
||||
: status === 'testing'
|
||||
? 'rgba(255,255,0,0.1)'
|
||||
: 'rgba(255,0,0,0.1)'
|
||||
}}>
|
||||
{status === 'success' && 'Successfully linked! You can now use Alexa.'}
|
||||
{status === 'connected' && 'Connection successful! Gateway is reachable.'}
|
||||
{status === 'testing' && 'Testing connection...'}
|
||||
{status === 'failed' && 'Connection failed. Check your URL and token.'}
|
||||
{status === 'error' && 'Error saving configuration. Please try again.'}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{
|
||||
marginTop: '30px',
|
||||
padding: '20px',
|
||||
background: 'rgba(0,0,0,0.2)',
|
||||
borderRadius: '10px'
|
||||
}}>
|
||||
<h3 style={{ margin: '0 0 10px 0', fontSize: '14px', color: '#888' }}>
|
||||
How to use:
|
||||
</h3>
|
||||
<ol style={{ margin: 0, paddingLeft: '20px', color: '#666', fontSize: '13px' }}>
|
||||
<li>Start your OpenClaw/Moltbot gateway</li>
|
||||
<li>Run ngrok: <code>ngrok http 18789</code></li>
|
||||
<li>Paste your ngrok URL and token above</li>
|
||||
<li>Say: "Alexa, ask smart claw..."</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
102
webapp/app/privacy/page.js
Normal file
102
webapp/app/privacy/page.js
Normal file
@@ -0,0 +1,102 @@
|
||||
export default function Privacy() {
|
||||
return (
|
||||
<main style={{
|
||||
maxWidth: '800px',
|
||||
margin: '0 auto',
|
||||
padding: '40px 20px',
|
||||
color: '#fff',
|
||||
lineHeight: '1.8'
|
||||
}}>
|
||||
<h1 style={{
|
||||
fontSize: '2.5rem',
|
||||
marginBottom: '30px',
|
||||
background: 'linear-gradient(90deg, #00d4ff, #7b2ff7)',
|
||||
WebkitBackgroundClip: 'text',
|
||||
WebkitTextFillColor: 'transparent'
|
||||
}}>
|
||||
Privacy Policy
|
||||
</h1>
|
||||
|
||||
<p style={{ color: '#888', marginBottom: '30px' }}>
|
||||
Last updated: January 30, 2026
|
||||
</p>
|
||||
|
||||
<section style={{ marginBottom: '30px' }}>
|
||||
<h2 style={{ color: '#00d4ff', marginBottom: '15px' }}>Overview</h2>
|
||||
<p>
|
||||
Smart Claw ("we", "our", or "the skill") is an Alexa skill that connects
|
||||
users to AI assistants. This privacy policy explains how we collect, use,
|
||||
and protect your information.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section style={{ marginBottom: '30px' }}>
|
||||
<h2 style={{ color: '#00d4ff', marginBottom: '15px' }}>Information We Collect</h2>
|
||||
<p>When you use Smart Claw, we may collect:</p>
|
||||
<ul style={{ marginLeft: '20px', marginTop: '10px' }}>
|
||||
<li><strong>Voice Commands:</strong> The questions and commands you speak to Alexa are processed to provide responses.</li>
|
||||
<li><strong>Account Linking Data:</strong> If you link your account, we store your gateway URL and authentication token to connect to your personal AI assistant.</li>
|
||||
<li><strong>Usage Data:</strong> Basic usage information to improve the skill.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section style={{ marginBottom: '30px' }}>
|
||||
<h2 style={{ color: '#00d4ff', marginBottom: '15px' }}>How We Use Your Information</h2>
|
||||
<ul style={{ marginLeft: '20px' }}>
|
||||
<li>To process your voice commands and provide AI responses</li>
|
||||
<li>To connect to your personal AI gateway (if linked)</li>
|
||||
<li>To improve and maintain the skill</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section style={{ marginBottom: '30px' }}>
|
||||
<h2 style={{ color: '#00d4ff', marginBottom: '15px' }}>Data Storage</h2>
|
||||
<p>
|
||||
Your account linking information is stored securely in AWS DynamoDB.
|
||||
Voice commands are processed in real-time and are not permanently stored by us.
|
||||
Amazon may retain voice recordings according to their own privacy policy.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section style={{ marginBottom: '30px' }}>
|
||||
<h2 style={{ color: '#00d4ff', marginBottom: '15px' }}>Data Sharing</h2>
|
||||
<p>
|
||||
We do not sell or share your personal information with third parties.
|
||||
Your voice commands may be sent to:
|
||||
</p>
|
||||
<ul style={{ marginLeft: '20px', marginTop: '10px' }}>
|
||||
<li>Your personal AI gateway (if you linked one)</li>
|
||||
<li>xAI/Grok API for AI processing (if no gateway is linked)</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section style={{ marginBottom: '30px' }}>
|
||||
<h2 style={{ color: '#00d4ff', marginBottom: '15px' }}>Your Rights</h2>
|
||||
<p>You can:</p>
|
||||
<ul style={{ marginLeft: '20px', marginTop: '10px' }}>
|
||||
<li>Unlink your account at any time through the Alexa app</li>
|
||||
<li>Disable the skill to stop all data collection</li>
|
||||
<li>Request deletion of your data by contacting us</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section style={{ marginBottom: '30px' }}>
|
||||
<h2 style={{ color: '#00d4ff', marginBottom: '15px' }}>Contact</h2>
|
||||
<p>
|
||||
For privacy questions or data deletion requests, contact us at:{' '}
|
||||
<a href="mailto:chmirenko2@gmail.com" style={{ color: '#00d4ff' }}>
|
||||
chmirenko2@gmail.com
|
||||
</a>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section style={{ marginBottom: '30px' }}>
|
||||
<h2 style={{ color: '#00d4ff', marginBottom: '15px' }}>Changes</h2>
|
||||
<p>
|
||||
We may update this privacy policy from time to time. Any changes will
|
||||
be posted on this page with an updated revision date.
|
||||
</p>
|
||||
</section>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
8
webapp/next.config.js
Normal file
8
webapp/next.config.js
Normal file
@@ -0,0 +1,8 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
experimental: {
|
||||
serverActions: true,
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = nextConfig
|
||||
17
webapp/package.json
Normal file
17
webapp/package.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "openclaw-alexa-webapp",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "^14.0.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"@aws-sdk/client-dynamodb": "^3.400.0",
|
||||
"@aws-sdk/lib-dynamodb": "^3.400.0"
|
||||
}
|
||||
}
|
||||
BIN
webapp/public/icon-108.png
Normal file
BIN
webapp/public/icon-108.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
BIN
webapp/public/icon-512.png
Normal file
BIN
webapp/public/icon-512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 85 KiB |
44
webapp/public/icon-512.svg
Normal file
44
webapp/public/icon-512.svg
Normal file
@@ -0,0 +1,44 @@
|
||||
<svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#1a1a2e"/>
|
||||
<stop offset="100%" style="stop-color:#16213e"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="accent" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" style="stop-color:#00d4ff"/>
|
||||
<stop offset="100%" style="stop-color:#7b2ff7"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- Background -->
|
||||
<rect width="512" height="512" rx="100" fill="url(#bg)"/>
|
||||
|
||||
<!-- Outer ring -->
|
||||
<circle cx="256" cy="256" r="180" fill="none" stroke="url(#accent)" stroke-width="8" opacity="0.3"/>
|
||||
|
||||
<!-- Claw shape -->
|
||||
<g transform="translate(256, 256)">
|
||||
<!-- Center circle (eye) -->
|
||||
<circle cx="0" cy="0" r="40" fill="url(#accent)"/>
|
||||
<circle cx="0" cy="0" r="20" fill="#1a1a2e"/>
|
||||
|
||||
<!-- Claw arms -->
|
||||
<path d="M -30 -80 Q -60 -120 -40 -160 Q -20 -180 10 -160 Q 30 -140 20 -100 Q 10 -70 0 -50"
|
||||
fill="none" stroke="url(#accent)" stroke-width="16" stroke-linecap="round"/>
|
||||
<path d="M 30 -80 Q 60 -120 40 -160 Q 20 -180 -10 -160 Q -30 -140 -20 -100 Q -10 -70 0 -50"
|
||||
fill="none" stroke="url(#accent)" stroke-width="16" stroke-linecap="round"/>
|
||||
|
||||
<!-- Bottom claws -->
|
||||
<path d="M -80 30 Q -120 60 -140 40 Q -160 20 -140 -10 Q -120 -30 -80 -20 Q -50 -10 -40 0"
|
||||
fill="none" stroke="url(#accent)" stroke-width="16" stroke-linecap="round"/>
|
||||
<path d="M 80 30 Q 120 60 140 40 Q 160 20 140 -10 Q 120 -30 80 -20 Q 50 -10 40 0"
|
||||
fill="none" stroke="url(#accent)" stroke-width="16" stroke-linecap="round"/>
|
||||
<path d="M 0 80 Q 0 120 -20 140 Q -40 160 -60 140 Q -70 120 -50 90 Q -30 60 0 50"
|
||||
fill="none" stroke="url(#accent)" stroke-width="16" stroke-linecap="round"/>
|
||||
<path d="M 0 80 Q 0 120 20 140 Q 40 160 60 140 Q 70 120 50 90 Q 30 60 0 50"
|
||||
fill="none" stroke="url(#accent)" stroke-width="16" stroke-linecap="round"/>
|
||||
</g>
|
||||
|
||||
<!-- Glow effect -->
|
||||
<circle cx="256" cy="256" r="60" fill="url(#accent)" opacity="0.1"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
5
webapp/vercel.json
Normal file
5
webapp/vercel.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"framework": "nextjs",
|
||||
"buildCommand": "npm run build",
|
||||
"outputDirectory": ".next"
|
||||
}
|
||||
Reference in New Issue
Block a user