OpenClaw Strava Plugin + Dom Agent Implementation Plan
For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
Goal: Build a standalone, distributable OpenClaw Strava plugin (11 tools + coaching skill) packaged as a GitHub Release, plus a Dom fitness coach agent for RockLobsta.
Architecture: Four parallel workstreams: (A) Plugin code in StravaMCP repo, (B) Skill SKILL.md, (C) Packaging + GitHub Release, (D) RockLobsta integration (Dom agent + config + infra). The plugin is a standalone package installable on any OpenClaw instance. Dom is RockLobsta-specific.
Tech Stack: TypeScript, Node 22 native fetch, @sinclair/typebox for schemas, OpenClaw plugin SDK, GitHub Actions, AWS CDK v2
Design doc: docs/plans/2026-02-13-openclaw-strava-plugin-design.md
Plugin source: /Volumes/DataDeuce/Projects/StravaMCP/openclaw-plugin/ RockLobsta repo: /Users/Stealinglight/Developer/Projects/RockLobsta
Important Conventions (Read First)
OpenClaw Plugin Conventions (from existing RockLobsta plugins)
- Plugin entry: export default object with
{ id, name, description, configSchema, register(api) } - Use
@sinclair/typeboxType.Object() for schemas (NOT Zod) - Use
api.registerTool()with signature:(toolDef, { name: string }) - Tool execute returns:
{ content: [{ type: "text", text: "..." }], details?: {} } - Use native
fetchfor HTTP (NOT axios - not in the OpenClaw runtime) - Plugin manifest:
openclaw.plugin.jsonwithid,configSchema,uiHints - Plugin package.json needs
"openclaw": { "extensions": ["./index.ts"] } - Skills bundled at
<plugin-root>/skills/<skill-name>/SKILL.md
Standalone Plugin Packaging
- Plugin is NOT part of the OpenClaw monorepo (no
"openclaw": "workspace:*") - Must declare
@sinclair/typeboxas a real dependency - OpenClaw plugin SDK types: use
openclaw/plugin-sdkimport (resolved at install time by OpenClaw) - Installation methods:
openclaw plugins install ./path(from extracted release)openclaw plugins install -l ./path(symlink for dev)- Place in
~/.openclaw/extensions/or<workspace>/.openclaw/extensions/ - For Docker: COPY into image and reference via
plugins.load.pathsin config
RockLobsta Conventions
- Workspace dirs:
docker/workspace-<agentId>/ - Required workspace files: SOUL.md, IDENTITY.md, AGENTS.md
- Container workspace path:
/home/node/.app/workspace-<agentId> - Token files:
/home/node/.app/tokens/<name>(EFS-persisted) - Config:
docker/config.json - Secrets: AWS Secrets Manager, injected by ECS task execution role
Parallel Workstreams
Workstream A: Plugin code (StravaMCP repo) --- Tasks 1-6
Workstream B: Skill (bundled with plugin) --- Task 7
Workstream C: Packaging + Release --- Tasks 8-9
Workstream D: RockLobsta integration --- Tasks 10-15
A and B can run fully in parallel.
C depends on A + B completing.
D (Tasks 10-13) can run in parallel with A + B.
D (Tasks 14-15) depend on C completing.
Workstream A: Plugin Code
All files created under /Volumes/DataDeuce/Projects/StravaMCP/openclaw-plugin/
Task 1: Scaffold Plugin Package
Files:
- Create:
openclaw-plugin/openclaw.plugin.json - Create:
openclaw-plugin/package.json - Create:
openclaw-plugin/tsconfig.json - Create:
openclaw-plugin/index.ts
Step 1: Create plugin manifest
Create openclaw-plugin/openclaw.plugin.json:
{
"id": "strava",
"configSchema": {
"type": "object",
"additionalProperties": false,
"properties": {
"tokenFile": {
"type": "string"
}
},
"required": ["tokenFile"]
},
"uiHints": {
"tokenFile": {
"label": "Strava Token File Path",
"placeholder": "~/.openclaw/tokens/strava",
"help": "Path to a JSON file containing Strava OAuth credentials (client_id, client_secret, access_token, refresh_token, expires_at). The plugin auto-refreshes and writes back updated tokens."
}
}
}
Step 2: Create package.json
Create openclaw-plugin/package.json:
{
"name": "@stealinglight/openclaw-strava",
"version": "1.0.0",
"description": "OpenClaw plugin providing 11 native Strava API tools for fitness data management, activity enrichment, and training analysis",
"type": "module",
"license": "MIT",
"author": "Chris McMillon <@stealinglight>",
"repository": {
"type": "git",
"url": "https://github.com/Stealinglight/StravaMCP"
},
"keywords": ["openclaw", "strava", "fitness", "plugin"],
"dependencies": {
"@sinclair/typebox": "^0.34.0"
},
"openclaw": {
"extensions": [
"./index.ts"
]
}
}
Step 3: Create tsconfig.json
Create openclaw-plugin/tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"esModuleInterop": true,
"outDir": "./dist",
"rootDir": ".",
"declaration": true,
"skipLibCheck": true
},
"include": ["./**/*.ts"],
"exclude": ["dist", "node_modules", "**/*.test.ts"]
}
Step 4: Create minimal plugin entry (skeleton)
Create openclaw-plugin/index.ts:
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import { Type } from "@sinclair/typebox";
const stravaPlugin = {
id: "strava",
name: "Strava",
description: "Native Strava fitness data integration with 11 tools for activity management, athlete stats, telemetry, clubs, and uploads",
configSchema: Type.Object({
tokenFile: Type.String({ description: "Path to JSON file containing Strava OAuth credentials" }),
}),
register(api: OpenClawPluginApi) {
api.logger.info("strava: plugin registered (tools loading...)");
},
};
export default stravaPlugin;
Step 5: Commit
git add openclaw-plugin/
git commit -m "feat(openclaw-plugin): scaffold standalone plugin package"
Task 2: Strava OAuth Client
Files:
- Create:
openclaw-plugin/src/token-store.ts - Create:
openclaw-plugin/src/strava-client.ts
Step 1: Create token store (file-backed, works on any filesystem)
Create openclaw-plugin/src/token-store.ts:
import { readFileSync, writeFileSync, existsSync } from "node:fs";
export interface StravaTokens {
client_id: string;
client_secret: string;
access_token: string;
refresh_token: string;
expires_at: number;
}
export class TokenStore {
constructor(private readonly filePath: string) {}
read(): StravaTokens {
if (!existsSync(this.filePath)) {
throw new Error(
`Strava token file not found: ${this.filePath}\n` +
`Create a JSON file at this path with: client_id, client_secret, access_token, refresh_token, expires_at`
);
}
const raw = readFileSync(this.filePath, "utf-8");
return JSON.parse(raw) as StravaTokens;
}
write(tokens: StravaTokens): void {
writeFileSync(this.filePath, JSON.stringify(tokens, null, 2), "utf-8");
}
isExpired(tokens: StravaTokens, bufferSeconds = 300): boolean {
return Date.now() / 1000 >= tokens.expires_at - bufferSeconds;
}
}
Step 2: Create Strava API client with auto-refresh
Create openclaw-plugin/src/strava-client.ts:
import type { TokenStore, StravaTokens } from "./token-store.js";
const STRAVA_API_BASE = "https://www.strava.com/api/v3";
const STRAVA_TOKEN_URL = "https://www.strava.com/oauth/token";
export class StravaClient {
private refreshPromise: Promise<StravaTokens> | null = null;
constructor(private readonly tokenStore: TokenStore) {}
private async ensureValidToken(): Promise<string> {
let tokens = this.tokenStore.read();
if (this.tokenStore.isExpired(tokens)) {
if (!this.refreshPromise) {
this.refreshPromise = this.refreshToken(tokens);
}
tokens = await this.refreshPromise;
this.refreshPromise = null;
}
return tokens.access_token;
}
private async refreshToken(tokens: StravaTokens): Promise<StravaTokens> {
const response = await fetch(STRAVA_TOKEN_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
client_id: tokens.client_id,
client_secret: tokens.client_secret,
refresh_token: tokens.refresh_token,
grant_type: "refresh_token",
}),
});
if (!response.ok) {
const body = await response.text();
throw new Error(`Strava token refresh failed (${response.status}): ${body}`);
}
const data = (await response.json()) as {
access_token: string;
refresh_token: string;
expires_at: number;
};
const updated: StravaTokens = {
...tokens,
access_token: data.access_token,
refresh_token: data.refresh_token,
expires_at: data.expires_at,
};
this.tokenStore.write(updated);
return updated;
}
async get(path: string, params?: Record<string, string | number | boolean>): Promise<unknown> {
const token = await this.ensureValidToken();
const url = new URL(`${STRAVA_API_BASE}${path}`);
if (params) {
for (const [key, value] of Object.entries(params)) {
if (value !== undefined && value !== null) {
url.searchParams.set(key, String(value));
}
}
}
const response = await fetch(url.toString(), {
headers: { Authorization: `Bearer ${token}` },
});
if (!response.ok) {
const body = await response.text();
throw new Error(`Strava API error (${response.status} ${path}): ${body}`);
}
return response.json();
}
async post(path: string, body: unknown): Promise<unknown> {
const token = await this.ensureValidToken();
const response = await fetch(`${STRAVA_API_BASE}${path}`, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
if (!response.ok) {
const text = await response.text();
throw new Error(`Strava API error (${response.status} ${path}): ${text}`);
}
return response.json();
}
async put(path: string, body: unknown): Promise<unknown> {
const token = await this.ensureValidToken();
const response = await fetch(`${STRAVA_API_BASE}${path}`, {
method: "PUT",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
if (!response.ok) {
const text = await response.text();
throw new Error(`Strava API error (${response.status} ${path}): ${text}`);
}
return response.json();
}
}
Step 3: Commit
git add openclaw-plugin/src/
git commit -m "feat(openclaw-plugin): add OAuth client with auto-refresh and file-backed token persistence"
Task 3: Activity Tools (4 tools)
Files:
- Create:
openclaw-plugin/src/tools/activities.ts
Step 1: Create activity tools (same code as previous plan, paths adjusted)
Create openclaw-plugin/src/tools/activities.ts with all 4 activity tools: strava_get_activities, strava_get_activity_by_id, strava_create_activity, strava_update_activity.
See previous plan Task 3 for full source code. All imports change from "openclaw/plugin-sdk" (unchanged) and "../strava-client.js" (unchanged).
Step 2: Commit
git add openclaw-plugin/src/tools/activities.ts
git commit -m "feat(openclaw-plugin): add activity tools (list, get, create, update)"
Task 4: Athlete, Streams, and Zones Tools (4 tools)
Files:
- Create:
openclaw-plugin/src/tools/athlete.ts - Create:
openclaw-plugin/src/tools/streams.ts - Create:
openclaw-plugin/src/tools/zones.ts
Same source code as previous plan Task 4, just at openclaw-plugin/ path.
Step: Commit
git add openclaw-plugin/src/tools/athlete.ts openclaw-plugin/src/tools/streams.ts openclaw-plugin/src/tools/zones.ts
git commit -m "feat(openclaw-plugin): add athlete, streams, and zones tools"
Task 5: Club and Upload Tools (3 tools)
Files:
- Create:
openclaw-plugin/src/tools/clubs.ts - Create:
openclaw-plugin/src/tools/uploads.ts
Same source code as previous plan Task 5, just at openclaw-plugin/ path.
Step: Commit
git add openclaw-plugin/src/tools/clubs.ts openclaw-plugin/src/tools/uploads.ts
git commit -m "feat(openclaw-plugin): add club and upload tools"
Task 6: Wire Up Plugin Entry Point
Files:
- Modify:
openclaw-plugin/index.ts
Replace with full entry point that imports and registers all 11 tools. Same code as previous plan Task 6.
Step: Commit
git add openclaw-plugin/index.ts
git commit -m "feat(openclaw-plugin): wire up all 11 tools in plugin entry point"
Workstream B: Strava Coaching Skill
Task 7: Create SKILL.md
Files:
- Create:
openclaw-plugin/skills/strava-coaching/SKILL.md
Same content as previous plan Task 7. Bundled with plugin so it auto-discovers when the plugin is installed.
Step: Commit
git add openclaw-plugin/skills/
git commit -m "feat(openclaw-plugin): add strava-coaching skill with activity update workflow"
Workstream C: Packaging + Distribution
Task 8: Create Plugin README
Files:
- Create:
openclaw-plugin/README.md
Create openclaw-plugin/README.md:
# OpenClaw Strava Plugin
Native Strava API integration for [OpenClaw](https://openclaw.ai). Provides 11 tools for managing activities, viewing athlete stats, analyzing telemetry, and more. Includes a bundled `strava-coaching` skill that guides agents through the activity enrichment workflow.
## Tools
| Tool | Description |
|------|-------------|
| `strava_get_activities` | List recent activities with date filtering |
| `strava_get_activity_by_id` | Get detailed activity info |
| `strava_create_activity` | Manually create activities |
| `strava_update_activity` | Update name/description/sport_type |
| `strava_get_activity_zones` | HR and power zone distribution |
| `strava_get_athlete` | Athlete profile |
| `strava_get_athlete_stats` | Training statistics (recent/YTD/all-time) |
| `strava_get_activity_streams` | Time-series telemetry data |
| `strava_get_club_activities` | Club member activities |
| `strava_create_upload` | Upload FIT/TCX/GPX files |
| `strava_get_upload` | Check upload processing status |
## Installation
### 1. Download the latest release
```bash
# From GitHub Releases
curl -L https://github.com/Stealinglight/StravaMCP/releases/latest/download/openclaw-strava-plugin.tar.gz -o openclaw-strava-plugin.tar.gz
tar xzf openclaw-strava-plugin.tar.gz
2. Install the plugin
# Install (copies to OpenClaw extensions)
openclaw plugins install ./openclaw-strava-plugin
# Or link for development
openclaw plugins install -l ./openclaw-strava-plugin
3. Create a Strava token file
You need a JSON file with your Strava OAuth credentials:
{
"client_id": "YOUR_CLIENT_ID",
"client_secret": "YOUR_CLIENT_SECRET",
"access_token": "YOUR_ACCESS_TOKEN",
"refresh_token": "YOUR_REFRESH_TOKEN",
"expires_at": 0
}
To get these credentials:
- Create a Strava API Application at https://www.strava.com/settings/api
- Use the existing StravaMCP
get-token.jsscript or any OAuth 2.0 flow to get a refresh token - Save the JSON file (e.g.
~/.openclaw/tokens/strava)
The plugin auto-refreshes tokens and writes updated credentials back to this file.
4. Enable in openclaw.json
{
"plugins": {
"allow": ["strava"],
"entries": {
"strava": {
"enabled": true,
"config": {
"tokenFile": "~/.openclaw/tokens/strava"
}
}
}
}
}
5. Use the coaching skill
The bundled strava-coaching skill is automatically available. Use /strava-coaching in chat or let your agent pick it up from the skill description.
Docker / Fargate Deployment
For containerized OpenClaw instances:
- COPY the plugin into the image:
COPY openclaw-strava-plugin/ /app/extensions/strava/ - Add to config:
{ "plugins": { "load": { "paths": ["/app/extensions/strava"] }, "entries": { "strava": { "enabled": true, "config": { "tokenFile": "/home/node/.app/tokens/strava" } } } } } - Seed the token file on the persistent volume (EFS, EBS, etc.)
Requirements
- OpenClaw with Node 22+ runtime
- Strava API application credentials
- Network access to
https://www.strava.com/api/v3```
Step: Commit
git add openclaw-plugin/README.md
git commit -m "docs(openclaw-plugin): add installation and usage README"
Task 9: GitHub Actions Release Workflow
Files:
- Create:
.github/workflows/release-openclaw-plugin.yml
Create .github/workflows/release-openclaw-plugin.yml:
name: Release OpenClaw Plugin
on:
push:
tags:
- 'openclaw-plugin-v*'
permissions:
contents: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Package plugin
run: |
cd openclaw-plugin
# Install dependencies
npm install --production
cd ..
# Create tarball of the plugin directory
tar czf openclaw-strava-plugin.tar.gz \
--transform 's,^openclaw-plugin,openclaw-strava-plugin,' \
openclaw-plugin/
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
files: openclaw-strava-plugin.tar.gz
generate_release_notes: true
body: |
## OpenClaw Strava Plugin
Download and install:
```bash
curl -L $/$/releases/download/$/openclaw-strava-plugin.tar.gz | tar xz
openclaw plugins install ./openclaw-strava-plugin
```
See [openclaw-plugin/README.md](openclaw-plugin/README.md) for full setup instructions.
Step: Commit
git add .github/workflows/release-openclaw-plugin.yml
git commit -m "ci: add GitHub Actions workflow for OpenClaw plugin releases"
Workstream D: RockLobsta Integration
All tasks in this workstream operate on /Users/Stealinglight/Developer/Projects/RockLobsta
Task 10: Create Dom’s Workspace Files
Files:
- Create:
docker/workspace-dom/IDENTITY.md - Create:
docker/workspace-dom/SOUL.md - Create:
docker/workspace-dom/USER.md - Create:
docker/workspace-dom/AGENTS.md - Create:
docker/workspace-dom/TOOLS.md
Same workspace file content as the previous plan Task 8. These are RockLobsta-specific and define Dom’s personality, Chris’s user profile, and agent operating instructions.
Step: Commit in RockLobsta repo
cd /Users/Stealinglight/Developer/Projects/RockLobsta
git add docker/workspace-dom/
git commit -m "feat(dom): create fitness coach agent workspace files"
Task 11: Update docker/config.json
Files:
- Modify:
docker/config.json
Changes (same as previous plan Task 9):
- Add Dom to
agents.list[] - Add
"dom"totools.agentToAgent.allow[] - Add
"dom"to main agent’ssubagents.allowAgents[] - Add Telegram binding for Dom (before main catch-all)
- Add Telegram account
"dom"with tokenFile - Add strava plugin to
plugins.allow[]andplugins.entries - Add
plugins.load.pathspointing to where the plugin will be installed:"plugins": { "allow": ["telegram", "strava"], "load": { "paths": ["/app/extensions/strava"] }, "entries": { "telegram": { "enabled": true }, "strava": { "enabled": true, "config": { "tokenFile": "/home/node/.app/tokens/strava" } } } }
Step: Commit
git add docker/config.json
git commit -m "feat(config): add Dom agent, Strava plugin, and Telegram binding"
Task 12: Update Dockerfile
Files:
- Modify:
docker/Dockerfile
Step 1: Add workspace-dom and plugin COPY lines
In the runtime stage, add:
# Dom fitness coach workspace
COPY docker/workspace-dom/ /app/workspace-dom/
# Strava plugin (downloaded from GitHub Release or copied from StravaMCP repo)
# During development, mount or copy from local. In CI, download from release.
COPY extensions/strava/ /app/extensions/strava/
Note: The extensions/strava/ directory in RockLobsta will contain the plugin files. For development, this can be a symlink or a copy from StravaMCP. For CI/CD, the build workflow can download the latest release.
Step 2: Commit
git add docker/Dockerfile
git commit -m "feat(docker): add Dom workspace and Strava plugin to container image"
Task 13: Update entrypoint.sh
Files:
- Modify:
docker/entrypoint.sh
Add Dom workspace bootstrap call alongside existing agent workspaces:
bootstrap_workspace "/app/workspace-dom" "/home/node/.app/workspace-dom"
Step: Commit
git add docker/entrypoint.sh
git commit -m "feat(entrypoint): bootstrap Dom workspace to EFS"
Task 14: CDK Infrastructure Updates
Files:
- Modify:
lib/foundation-stack.ts - Modify:
lib/constructs/bot-iam-roles.ts
Same changes as previous plan Task 12:
- Add
openclaw-dom-telegram-tokenandopenclaw-strava-credentialssecrets in FoundationStack - Add
domTelegramSecretArnandstravaSecretArnprops to BotIamRoles - Add IAM policy statements granting execution role access to new secrets
- Wire up and expose as public properties
Step: Verify and commit
npm run build && npx cdk diff
git add lib/foundation-stack.ts lib/constructs/bot-iam-roles.ts
git commit -m "feat(cdk): add Strava and Dom Telegram secrets with IAM permissions"
Task 15: Copy Plugin Into RockLobsta for First Build
Files:
- Create:
extensions/strava/(copy from StravaMCP/openclaw-plugin/)
For the initial deployment before GitHub Releases exist:
# From RockLobsta root
mkdir -p extensions/strava
cp -r /Volumes/DataDeuce/Projects/StravaMCP/openclaw-plugin/* extensions/strava/
cd extensions/strava && npm install --production && cd ../..
git add extensions/strava/
git commit -m "feat: add Strava OpenClaw plugin (initial copy from StravaMCP)"
Future deployments: update build-push.yml to download the latest release tarball instead of maintaining a copy.
Post-Implementation
Task 16: Seed Strava Token (Manual)
Chris must:
- Create a Telegram bot for Dom via @BotFather, store token in Secrets Manager as
openclaw-dom-telegram-token - Create a Strava token file with OAuth credentials and store in Secrets Manager as
openclaw-strava-credentials - Update
docker/entrypoint.shto fetch the Strava secret and write to/home/node/.app/tokens/stravaon startup (same pattern as Telegram token fetching)
Task 17: Tag and Release
After all code is merged:
cd /Volumes/DataDeuce/Projects/StravaMCP
git tag openclaw-plugin-v1.0.0
git push origin openclaw-plugin-v1.0.0
This triggers the GitHub Actions workflow to create a release with the downloadable tarball.
File Summary
StravaMCP Repo - New Files (Plugin Package)
openclaw-plugin/
openclaw.plugin.json
package.json
tsconfig.json
README.md
index.ts
src/
token-store.ts
strava-client.ts
tools/
activities.ts
athlete.ts
streams.ts
zones.ts
clubs.ts
uploads.ts
skills/
strava-coaching/
SKILL.md
.github/workflows/
release-openclaw-plugin.yml
RockLobsta Repo - New Files (Dom Agent)
docker/workspace-dom/
IDENTITY.md
SOUL.md
USER.md
AGENTS.md
TOOLS.md
extensions/strava/ (copy of plugin for initial build)
RockLobsta Repo - Modified Files
docker/config.json (add Dom + Strava plugin config)
docker/Dockerfile (add workspace-dom + plugin COPY)
docker/entrypoint.sh (add Dom workspace bootstrap)
lib/foundation-stack.ts (add secrets)
lib/constructs/bot-iam-roles.ts (add IAM permissions)