Initial commit: OpenClaw Alexa Skill

This commit is contained in:
pchmirenko
2026-01-30 21:14:19 +01:00
commit 1eafd72c63
22 changed files with 1460 additions and 0 deletions

10
.env.example Normal file
View 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
View 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
View 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
View 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
View 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'"

View 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
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 508 KiB

40
skill.json Normal file
View 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
View File

@@ -0,0 +1,5 @@
node_modules
.next
.env.local
.env
.vercel

View 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 });
}
}

View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,8 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
serverActions: true,
},
}
module.exports = nextConfig

17
webapp/package.json Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
webapp/public/icon-512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

View 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
View File

@@ -0,0 +1,5 @@
{
"framework": "nextjs",
"buildCommand": "npm run build",
"outputDirectory": ".next"
}