@clawhub-lovefromio-928f3848cd
Persistent memory system - automatic context capture and semantic search
---
name: openclaw-persistent-memory
version: 0.1.0
description: Persistent memory system - automatic context capture and semantic search
author: Jason Brashear / Titanium Computing
repository: https://github.com/webdevtodayjason/openclaw_memory
metadata:
openclaw:
requires:
bins: ["openclaw-persistent-memory"]
install:
- id: node
kind: node
package: openclaw-persistent-memory
bins: ["openclaw-persistent-memory"]
label: "Install OpenClaw Persistent Memory (npm)"
---
# OpenClaw Persistent Memory
Persistent memory system that automatically captures context across sessions using SQLite + FTS5.
## Features
- 🧠 **Auto-capture** - Important observations saved automatically after each response
- 🔍 **Auto-recall** - Relevant memories injected before each prompt
- 💾 **SQLite + FTS5** - Fast full-text search across all memories
- 🛠️ **Tools** - `memory_search`, `memory_get`, `memory_store`, `memory_delete`
- 📊 **Progressive disclosure** - Token-efficient retrieval
## Setup
1. **Install the npm package:**
```bash
npm install -g openclaw-persistent-memory
```
2. **Start the worker service:**
```bash
openclaw-persistent-memory start
```
3. **Install the OpenClaw extension:**
```bash
# Copy extension to OpenClaw extensions directory
cp -r node_modules/openclaw-persistent-memory/extension ~/.openclaw/extensions/openclaw-mem
cd ~/.openclaw/extensions/openclaw-mem && npm install
```
4. **Configure OpenClaw** (in `~/.openclaw/openclaw.json`):
```json
{
"plugins": {
"slots": {
"memory": "openclaw-mem"
},
"allow": ["openclaw-mem"],
"entries": {
"openclaw-mem": {
"enabled": true,
"config": {
"workerUrl": "http://127.0.0.1:37778",
"autoCapture": true,
"autoRecall": true
}
}
}
}
}
```
5. **Restart OpenClaw gateway**
## Tools Provided
| Tool | Description |
|------|-------------|
| `memory_search` | Search memories with natural language |
| `memory_get` | Get a specific memory by ID |
| `memory_store` | Save important information |
| `memory_delete` | Delete a memory by ID |
## API Endpoints
Worker runs on `http://127.0.0.1:37778`:
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/api/health` | GET | Health check |
| `/api/stats` | GET | Database statistics |
| `/api/search` | POST | Full-text search |
| `/api/observations` | GET | List recent observations |
| `/api/observations/:id` | GET | Get observation |
| `/api/observations/:id` | DELETE | Delete observation |
| `/api/observations/:id` | PATCH | Update observation |
## Troubleshooting
### Worker not running
```bash
curl http://127.0.0.1:37778/api/health
# If fails, restart:
openclaw-persistent-memory start
```
### Auto-recall not working
- Check OpenClaw logs: `tail ~/.openclaw/logs/*.log | grep openclaw-mem`
- Verify `plugins.slots.memory` is set to `"openclaw-mem"`
- Restart gateway after config changes
FILE:INSTALL.md
# Installation Guide
## Quick Start (5 minutes)
### 1. Install the npm package
```bash
npm install -g openclaw-persistent-memory
```
### 2. Start the worker service
```bash
# Start in foreground (for testing)
openclaw-persistent-memory start
# Or run in background
nohup openclaw-persistent-memory start &
```
Verify it's running:
```bash
curl http://127.0.0.1:37778/api/health
# Should return: {"status":"ok","timestamp":"..."}
```
### 3. Install the OpenClaw extension
```bash
# Get the package location
PKG_PATH=$(npm root -g)/openclaw-persistent-memory
# Create extension directory
mkdir -p ~/.openclaw/extensions/openclaw-mem
# Copy extension files
cp $PKG_PATH/extension/index.ts ~/.openclaw/extensions/openclaw-mem/
cp $PKG_PATH/extension/openclaw.plugin.json ~/.openclaw/extensions/openclaw-mem/
cp $PKG_PATH/extension/package.json ~/.openclaw/extensions/openclaw-mem/
# Install extension dependencies
cd ~/.openclaw/extensions/openclaw-mem && npm install
```
### 4. Configure OpenClaw
Edit `~/.openclaw/openclaw.json` and add/update the plugins section:
```json
{
"plugins": {
"slots": {
"memory": "openclaw-mem"
},
"allow": ["openclaw-mem"],
"entries": {
"openclaw-mem": {
"enabled": true,
"config": {
"workerUrl": "http://127.0.0.1:37778",
"autoCapture": true,
"autoRecall": true,
"maxContextTokens": 4000
}
}
}
}
}
```
### 5. Restart OpenClaw
```bash
openclaw gateway restart
```
---
## Automated Install Script
For convenience, you can use the install script:
```bash
curl -fsSL https://raw.githubusercontent.com/webdevtodayjason/openclaw_memory/main/skill/install.sh | bash
```
Or if you have the skill installed via ClawHub:
```bash
./scripts/install.sh
```
---
## Verify Installation
### Check worker status
```bash
curl http://127.0.0.1:37778/api/stats
# Should show sessions and observations count
```
### Check OpenClaw logs
```bash
tail ~/.openclaw/logs/*.log | grep openclaw-mem
# Should show: "openclaw-mem: plugin registered"
```
### Test auto-recall
Send a message to your agent, then check logs:
```bash
tail ~/.openclaw/logs/*.log | grep "inject"
# Should show memory injection when relevant
```
---
## Troubleshooting
### Worker won't start
```bash
# Check if port is in use
lsof -i :37778
# Kill existing process
kill $(lsof -t -i:37778)
# Restart worker
openclaw-persistent-memory start
```
### Extension not loading
```bash
# Check extension directory
ls ~/.openclaw/extensions/openclaw-mem/
# Should have: index.ts, openclaw.plugin.json, package.json, node_modules/
# If missing node_modules:
cd ~/.openclaw/extensions/openclaw-mem && npm install
```
### Auto-recall not working
1. Check `plugins.slots.memory` is set to `"openclaw-mem"` (not `"memory-core"`)
2. Restart gateway after config changes
3. Check logs for errors: `tail ~/.openclaw/logs/*.log | grep -i error`
---
## Uninstall
```bash
# Remove extension
rm -rf ~/.openclaw/extensions/openclaw-mem
# Remove from openclaw.json plugins.slots.memory and plugins.entries.openclaw-mem
# Stop worker
kill $(lsof -t -i:37778)
# Uninstall npm package
npm uninstall -g openclaw-persistent-memory
# Optional: remove database
rm -rf ~/.openclaw-mem/
```
FILE:README.md
# OpenClaw Persistent Memory Skill
Give your AI agent long-term memory with automatic context capture and recall.
## Features
- 🧠 **Auto-capture** - Important observations saved automatically
- 🔍 **Auto-recall** - Relevant memories injected before each prompt
- 💾 **SQLite + FTS5** - Fast full-text search
- 🛠️ **Tools** - `memory_search`, `memory_get`, `memory_store`, `memory_delete`
## Installation
### Via ClawHub (Recommended)
```bash
clawhub install openclaw-persistent-memory
```
### Via npm
```bash
npm install -g openclaw-persistent-memory
```
Then follow [INSTALL.md](./INSTALL.md) for extension setup.
## Quick Links
- [Installation Guide](./INSTALL.md)
- [Full Documentation](./SKILL.md)
- [GitHub Repository](https://github.com/webdevtodayjason/openclaw_memory)
- [npm Package](https://www.npmjs.com/package/openclaw-persistent-memory)
## License
AGPL-3.0
FILE:_meta.json
{
"ownerId": "kn71bs8wsmed8s9rnh0xnz1q8n809vnr",
"slug": "lovefromio-openclaw-persistent-memory",
"version": "0.1.3",
"publishedAt": 1770180617153
}
FILE:package.json
{
"name": "@openclaw/persistent-memory-skill",
"version": "0.1.0",
"description": "OpenClaw skill for persistent memory - automatic context capture, semantic search, auto-recall",
"keywords": [
"openclaw",
"memory",
"ai-agents",
"persistent-memory",
"fts5",
"sqlite",
"auto-recall",
"skill"
],
"homepage": "https://github.com/webdevtodayjason/openclaw_memory#readme",
"bugs": {
"url": "https://github.com/webdevtodayjason/openclaw_memory/issues"
},
"repository": {
"type": "git",
"url": "git+https://github.com/webdevtodayjason/openclaw_memory.git"
},
"license": "AGPL-3.0",
"author": "Jason Brashear / Titanium Computing",
"type": "module",
"files": [
"SKILL.md",
"INSTALL.md",
"README.md",
"scripts/"
],
"bin": {
"openclaw-mem-skill": "./scripts/mem-search.sh"
},
"engines": {
"node": ">=18"
},
"publishConfig": {
"access": "public"
}
}
FILE:scripts/install.sh
#!/bin/bash
# OpenClaw Persistent Memory - Install Script
# Installs npm package, sets up extension, and configures OpenClaw
set -e
echo "🧠 Installing OpenClaw Persistent Memory..."
# 1. Install npm package
echo "📦 Installing npm package..."
npm install -g openclaw-persistent-memory
# 2. Get package location
PKG_PATH=$(npm root -g)/openclaw-persistent-memory
# 3. Create extension directory
EXTENSION_DIR="$HOME/.openclaw/extensions/openclaw-mem"
echo "📁 Setting up extension at $EXTENSION_DIR..."
mkdir -p "$EXTENSION_DIR"
# 4. Copy extension files
cp "$PKG_PATH/extension/index.ts" "$EXTENSION_DIR/"
cp "$PKG_PATH/extension/openclaw.plugin.json" "$EXTENSION_DIR/"
cp "$PKG_PATH/extension/package.json" "$EXTENSION_DIR/"
# 5. Install extension dependencies
echo "📥 Installing extension dependencies..."
cd "$EXTENSION_DIR" && npm install
# 6. Check if openclaw.json exists
CONFIG_FILE="$HOME/.openclaw/openclaw.json"
if [ -f "$CONFIG_FILE" ]; then
echo "⚙️ OpenClaw config found at $CONFIG_FILE"
echo ""
echo "Add this to your plugins config:"
echo ' "plugins": {'
echo ' "slots": { "memory": "openclaw-mem" },'
echo ' "allow": ["openclaw-mem"],'
echo ' "entries": {'
echo ' "openclaw-mem": {'
echo ' "enabled": true,'
echo ' "config": {'
echo ' "workerUrl": "http://127.0.0.1:37778",'
echo ' "autoCapture": true,'
echo ' "autoRecall": true'
echo ' }'
echo ' }'
echo ' }'
echo ' }'
else
echo "⚠️ No OpenClaw config found. Run 'openclaw configure' first."
fi
echo ""
echo "✅ Installation complete!"
echo ""
echo "Next steps:"
echo " 1. Start the worker: openclaw-persistent-memory start"
echo " 2. Update openclaw.json with the config above"
echo " 3. Restart gateway: openclaw gateway restart"
echo ""
echo "Test it:"
echo " curl http://127.0.0.1:37778/api/health"
FILE:scripts/mem-search.sh
#!/bin/bash
# OpenClaw-Mem Search CLI
# Usage: mem-search.sh <query> [--type TYPE] [--limit N]
WORKER_URL="-http://127.0.0.1:37778"
show_help() {
echo "OpenClaw-Mem Search"
echo ""
echo "Usage: mem-search.sh <command> [options]"
echo ""
echo "Commands:"
echo " search <query> Search observations"
echo " get <id> Get observation by ID"
echo " timeline <id> Get timeline around observation"
echo " stats Show database statistics"
echo " health Check worker health"
echo ""
echo "Options:"
echo " --type TYPE Filter by type (bugfix, decision, etc.)"
echo " --limit N Max results (default: 10)"
echo " --json Output raw JSON"
}
check_worker() {
if ! curl -s "$WORKER_URL/api/health" > /dev/null 2>&1; then
echo "Error: OpenClaw-Mem worker not running"
echo "Start it with: openclaw-mem start-daemon"
exit 1
fi
}
search() {
local query="$1"
local type=""
local limit=10
local json=false
shift
while [[ $# -gt 0 ]]; do
case $1 in
--type) type="$2"; shift 2 ;;
--limit) limit="$2"; shift 2 ;;
--json) json=true; shift ;;
*) shift ;;
esac
done
local body="{\"query\": \"$query\", \"limit\": $limit"
if [[ -n "$type" ]]; then
body="$body, \"type\": \"$type\""
fi
body="$body}"
local result=$(curl -s -X POST "$WORKER_URL/api/search" \
-H "Content-Type: application/json" \
-d "$body")
if $json; then
echo "$result" | jq
else
echo "$result" | jq -r '.results[] | "#\(.id) [\(.type)] \(.created_at | split(" ")[0])\n \(.summary // .tool_name // "No summary")\n"'
fi
}
get_observation() {
local id="$1"
local json=false
if [[ "$2" == "--json" ]]; then
json=true
fi
local result=$(curl -s "$WORKER_URL/api/observations/$id")
if $json; then
echo "$result" | jq
else
echo "$result" | jq -r '"#\(.id) [\(.type)] \(.created_at)\nTool: \(.tool_name // "N/A")\nImportance: \(.importance)\n\nInput:\n\(.input // "N/A")\n\nOutput:\n\(.output // "N/A")\n\nSummary:\n\(.summary // "N/A")"'
fi
}
timeline() {
local id="$1"
local json=false
if [[ "$2" == "--json" ]]; then
json=true
fi
local result=$(curl -s -X POST "$WORKER_URL/api/timeline" \
-H "Content-Type: application/json" \
-d "{\"observation_id\": $id}")
if $json; then
echo "$result" | jq
else
echo "Timeline around #$id:"
echo ""
echo "$result" | jq -r '.observations[] | "#\(.id) \(.created_at) [\(.type)] \(.summary // .tool_name // "...")"'
fi
}
stats() {
local result=$(curl -s "$WORKER_URL/api/stats")
echo "$result" | jq -r '"OpenClaw-Mem Statistics\n\nSessions: \(.totalSessions)\nObservations: \(.totalObservations)\n\nBy Type:"'
echo "$result" | jq -r '.observationsByType | to_entries[] | " \(.key): \(.value)"'
}
health() {
curl -s "$WORKER_URL/api/health" | jq
}
# Main
case "-help" in
search)
check_worker
shift
search "$@"
;;
get)
check_worker
get_observation "$2" "$3"
;;
timeline)
check_worker
timeline "$2" "$3"
;;
stats)
check_worker
stats
;;
health)
health
;;
help|--help|-h)
show_help
;;
*)
echo "Unknown command: $1"
show_help
exit 1
;;
esac
Speak responses aloud on macOS using the built-in `say` command when user input indicates Voice Wake/voice recognition (for example, messages starting with "...
---
name: voice-wake-say
description: Speak responses aloud on macOS using the built-in `say` command when user input indicates Voice Wake/voice recognition (for example, messages starting with "User talked via voice recognition on <device>").
---
# Voice Wake Say
## Overview
Use macOS `say` to read the assistant's response out loud whenever the conversation came from Voice Wake/voice recognition. Do **not** use the `tts` tool (it calls cloud providers).
## When to Use `say` (CHECK EVERY MESSAGE INDIVIDUALLY)
**IF** the user message STARTS WITH: `User talked via voice recognition`
- **Step 1:** Acknowledge with `say` first (so the user knows you heard them)
- **Step 2:** Then perform the task
- **Step 3:** Optionally speak again when done if it makes sense
**IF** the user message does NOT start with that exact phrase
- THEN: Do NOT use `say`. Text-only response only.
**Critical:**
- Check EACH message individually — context does NOT carry over
- The trigger phrase must be at the VERY START of the message
- For tasks that take time, acknowledge FIRST so the user knows you're working
## Workflow
1) Detect Voice Wake context
- Trigger ONLY when the latest user/system message STARTS WITH `User talked via voice recognition`
- If the message instructs "repeat prompt first", keep that behavior in the response.
2) Prepare spoken text
- Use the final response text as the basis.
- Strip markdown/code blocks; if the response is long or code-heavy, speak a short summary and mention that details are on screen.
3) Speak with `say` (local macOS TTS)
```bash
printf '%s' "$SPOKEN_TEXT" | say
```
Optional controls (use only if set):
```bash
printf '%s' "$SPOKEN_TEXT" | say -v "$SAY_VOICE"
printf '%s' "$SPOKEN_TEXT" | say -r "$SAY_RATE"
```
## Failure handling
- If `say` is unavailable or errors, still send the text response and note that TTS failed.
FILE:_meta.json
{
"ownerId": "kn77k3j7wa1fedgd8g1fyz6ydh7z28mv",
"slug": "lovefromio-voice-wake-say",
"version": "1.0.1",
"publishedAt": 1769336291994
}OpenClaw skill for designing Telegram Bot API workflows and command-driven conversations using direct HTTPS requests (no SDKs).
---
name: telegram
description: OpenClaw skill for designing Telegram Bot API workflows and command-driven conversations using direct HTTPS requests (no SDKs).
---
# Telegram Bot Skill (Advanced)
## Purpose
Provide a clean, production-oriented guide for building Telegram bot workflows via the Bot API, focusing on command UX, update handling, and safe operations using plain HTTPS.
## Best fit
- You want a command-first bot that behaves professionally.
- You need a reliable update flow (webhook or polling).
- You prefer direct HTTP calls instead of libraries.
## Not a fit
- You require a full SDK or framework integration.
- You need complex media uploads and streaming in-process.
## Quick orientation
- Read `references/telegram-bot-api.md` for endpoints, update types, and request patterns.
- Read `references/telegram-commands-playbook.md` for command UX and messaging style.
- Read `references/telegram-update-routing.md` for update normalization and routing rules.
- Read `references/telegram-request-templates.md` for HTTP payload templates.
- Keep this SKILL.md short and use references for details.
## Required inputs
- Bot token and base API URL.
- Update strategy: webhook or long polling.
- Command list and conversation tone.
- Allowed update types and rate-limit posture.
## Expected output
- A clear command design, update flow plan, and operational checklist.
## Operational notes
- Prefer strict command routing: `/start`, `/help`, `/settings`, `/status`.
- Always validate incoming update payloads and chat context.
- Handle 429s with backoff and avoid message bursts.
## Security notes
- Never log tokens.
- Use webhooks with a secret token header when possible.
FILE:_meta.json
{
"ownerId": "kn7ehv4at8yekzag31spcarxm180bev0",
"slug": "lovefromio-telegram",
"version": "1.0.1",
"publishedAt": 1770028389996
}
FILE:references/telegram-bot-api.md
# Telegram Bot API Field Notes
## 1) Base URL and request style
- Base format: `https://api.telegram.org/bot<token>/<method>`
- Use GET or POST with JSON or form-encoded payloads.
- File uploads use `multipart/form-data` and `attach://` references.
## 2) Updates and delivery models
### Long polling
- `getUpdates` delivers updates with an `offset` cursor and `timeout`.
### Webhook
- `setWebhook` switches the bot to webhook mode.
- Webhook URLs must be HTTPS. Check the official docs for port restrictions.
### Update types (examples)
- `message`, `edited_message`, `channel_post`, `edited_channel_post`
- `inline_query`, `chosen_inline_result`, `callback_query`
- `shipping_query`, `pre_checkout_query`, `poll`, `poll_answer`
Use `allowed_updates` to limit which updates you receive.
## 3) High-traffic-safe patterns
- Use `allowed_updates` to reduce noise.
- Keep handlers idempotent (Telegram may retry).
- Return quickly from webhooks; process heavy work async.
## 4) Common methods (non-exhaustive)
- `getMe`, `getUpdates`, `setWebhook`
- `sendMessage`, `editMessageText`, `deleteMessage`
- `sendPhoto`, `sendDocument`, `sendChatAction`
- `answerCallbackQuery`, `answerInlineQuery`
## 5) Common fields (non-exhaustive)
### sendMessage
- `chat_id`, `text`, `parse_mode`
- `entities`, `disable_web_page_preview`
- `reply_markup` (inline keyboard, reply keyboard)
### reply_markup (inline keyboard)
- `inline_keyboard`: array of button rows
- Buttons can contain `text` + `callback_data` or `url`
### callback_query
- `id`, `from`, `message`, `data`
### sendChatAction
- `action`: `typing`, `upload_photo`, `upload_document`, `upload_video`, `choose_sticker`
## 6) Command UX checklist
- `/start`: greet, explain features, and show main commands.
- `/help`: include short examples and support contact.
- `/settings`: show toggles with inline keyboards.
- `/status`: show recent job results or queue size.
## 7) Error handling
- `429`: back off and retry.
- `400`: validate chat_id, message length, and formatting.
- `403`: bot blocked or chat not accessible.
## 8) Reference links
- https://core.telegram.org/bots/api
- https://core.telegram.org/bots/faq
FILE:references/telegram-commands-playbook.md
# Telegram Command Playbook
## Command set (professional baseline)
- `/start`: greet, set expectations, and show main actions.
- `/help`: short help + examples.
- `/status`: show last job result, queue length, or uptime.
- `/settings`: show toggles via inline keyboard.
- `/about`: short bot description and support contact.
## Command UX patterns
- Acknowledge fast, then do heavy work asynchronously.
- Prefer short replies with a single call-to-action.
- Always include “what next?” in `/start` and `/help`.
## Inline keyboard patterns
- Use stable callback_data names (e.g., `settings:notifications:on`).
- Keep callbacks idempotent.
## Message style guidelines
- Use MarkdownV2 or HTML consistently; avoid mixing.
- If using MarkdownV2, escape reserved characters.
- Keep single message length under safe limits; split when needed.
## Examples (short)
- `/start` reply: “Hi! I can publish posts and send alerts. Try /help.”
- `/status` reply: “Queue: 2 jobs. Last run: success 2m ago.”
FILE:references/telegram-request-templates.md
# Telegram Request Templates (HTTP)
## sendMessage
POST `/sendMessage`
```json
{
"chat_id": 123456789,
"text": "Hello",
"parse_mode": "HTML",
"disable_web_page_preview": true
}
```
## editMessageText
POST `/editMessageText`
```json
{
"chat_id": 123456789,
"message_id": 42,
"text": "Updated",
"parse_mode": "HTML"
}
```
## answerCallbackQuery
POST `/answerCallbackQuery`
```json
{
"callback_query_id": "1234567890",
"text": "Saved"
}
```
## setWebhook
POST `/setWebhook`
```json
{
"url": "https://example.com/telegram/webhook",
"secret_token": "your-secret",
"allowed_updates": ["message","callback_query"]
}
```
FILE:references/telegram-update-routing.md
# Telegram Update Routing
## Update normalization
- Normalize inbound updates to a single envelope:
- `update_id`, `chat_id`, `user_id`, `message_id`, `text`, `callback_data`, `type`
- This makes routing logic consistent across message types.
## Routing rules
- If `callback_query` exists, handle callbacks first.
- Else if `message.text` starts with `/`, treat as command.
- Else fall back to default handler (help or menu).
## Safe defaults
- Unknown command: reply with `/help` guidance.
- Unknown callback: answerCallbackQuery with a short notice.
## Idempotency
- Keep a cache of processed `update_id` in case of retries.
- Ensure handlers can be safely re-run.
## Error handling
- On 429: backoff and retry with jitter.
- On 400: validate payload length and parse mode.
Control Sonos speakers (discover/status/play/volume/group).
---
name: sonoscli
description: Control Sonos speakers (discover/status/play/volume/group).
homepage: https://sonoscli.sh
metadata: {"clawdbot":{"emoji":"🔊","requires":{"bins":["sonos"]},"install":[{"id":"go","kind":"go","module":"github.com/steipete/sonoscli/cmd/sonos@latest","bins":["sonos"],"label":"Install sonoscli (go)"}]}}
---
# Sonos CLI
Use `sonos` to control Sonos speakers on the local network.
Quick start
- `sonos discover`
- `sonos status --name "Kitchen"`
- `sonos play|pause|stop --name "Kitchen"`
- `sonos volume set 15 --name "Kitchen"`
Common tasks
- Grouping: `sonos group status|join|unjoin|party|solo`
- Favorites: `sonos favorites list|open`
- Queue: `sonos queue list|play|clear`
- Spotify search (via SMAPI): `sonos smapi search --service "Spotify" --category tracks "query"`
Notes
- If SSDP fails, specify `--ip <speaker-ip>`.
- Spotify Web API search is optional and requires `SPOTIFY_CLIENT_ID/SECRET`.
FILE:_meta.json
{
"ownerId": "kn70pywhg0fyz996kpa8xj89s57yhv26",
"slug": "lovefromio-sonoscli",
"version": "1.0.0",
"publishedAt": 1767545381030
}Captures learnings, errors, and corrections to enable continuous improvement. Use when: (1) A command or operation fails unexpectedly, (2) User corrects Clau...
---
name: self-improvement
description: "Captures learnings, errors, and corrections to enable continuous improvement. Use when: (1) A command or operation fails unexpectedly, (2) User corrects Claude ('No, that's wrong...', 'Actually...'), (3) User requests a capability that doesn't exist, (4) An external API or tool fails, (5) Claude realizes its knowledge is outdated or incorrect, (6) A better approach is discovered for a recurring task. Also review learnings before major tasks."
metadata:
---
# Self-Improvement Skill
Log learnings and errors to markdown files for continuous improvement. Coding agents can later process these into fixes, and important learnings get promoted to project memory.
## Quick Reference
| Situation | Action |
|-----------|--------|
| Command/operation fails | Log to `.learnings/ERRORS.md` |
| User corrects you | Log to `.learnings/LEARNINGS.md` with category `correction` |
| User wants missing feature | Log to `.learnings/FEATURE_REQUESTS.md` |
| API/external tool fails | Log to `.learnings/ERRORS.md` with integration details |
| Knowledge was outdated | Log to `.learnings/LEARNINGS.md` with category `knowledge_gap` |
| Found better approach | Log to `.learnings/LEARNINGS.md` with category `best_practice` |
| Simplify/Harden recurring patterns | Log/update `.learnings/LEARNINGS.md` with `Source: simplify-and-harden` and a stable `Pattern-Key` |
| Similar to existing entry | Link with `**See Also**`, consider priority bump |
| Broadly applicable learning | Promote to `CLAUDE.md`, `AGENTS.md`, and/or `.github/copilot-instructions.md` |
| Workflow improvements | Promote to `AGENTS.md` (OpenClaw workspace) |
| Tool gotchas | Promote to `TOOLS.md` (OpenClaw workspace) |
| Behavioral patterns | Promote to `SOUL.md` (OpenClaw workspace) |
## OpenClaw Setup (Recommended)
OpenClaw is the primary platform for this skill. It uses workspace-based prompt injection with automatic skill loading.
### Installation
**Via ClawdHub (recommended):**
```bash
clawdhub install self-improving-agent
```
**Manual:**
```bash
git clone https://github.com/peterskoett/self-improving-agent.git ~/.openclaw/skills/self-improving-agent
```
Remade for openclaw from original repo : https://github.com/pskoett/pskoett-ai-skills - https://github.com/pskoett/pskoett-ai-skills/tree/main/skills/self-improvement
### Workspace Structure
OpenClaw injects these files into every session:
```
~/.openclaw/workspace/
├── AGENTS.md # Multi-agent workflows, delegation patterns
├── SOUL.md # Behavioral guidelines, personality, principles
├── TOOLS.md # Tool capabilities, integration gotchas
├── MEMORY.md # Long-term memory (main session only)
├── memory/ # Daily memory files
│ └── YYYY-MM-DD.md
└── .learnings/ # This skill's log files
├── LEARNINGS.md
├── ERRORS.md
└── FEATURE_REQUESTS.md
```
### Create Learning Files
```bash
mkdir -p ~/.openclaw/workspace/.learnings
```
Then create the log files (or copy from `assets/`):
- `LEARNINGS.md` — corrections, knowledge gaps, best practices
- `ERRORS.md` — command failures, exceptions
- `FEATURE_REQUESTS.md` — user-requested capabilities
### Promotion Targets
When learnings prove broadly applicable, promote them to workspace files:
| Learning Type | Promote To | Example |
|---------------|------------|---------|
| Behavioral patterns | `SOUL.md` | "Be concise, avoid disclaimers" |
| Workflow improvements | `AGENTS.md` | "Spawn sub-agents for long tasks" |
| Tool gotchas | `TOOLS.md` | "Git push needs auth configured first" |
### Inter-Session Communication
OpenClaw provides tools to share learnings across sessions:
- **sessions_list** — View active/recent sessions
- **sessions_history** — Read another session's transcript
- **sessions_send** — Send a learning to another session
- **sessions_spawn** — Spawn a sub-agent for background work
### Optional: Enable Hook
For automatic reminders at session start:
```bash
# Copy hook to OpenClaw hooks directory
cp -r hooks/openclaw ~/.openclaw/hooks/self-improvement
# Enable it
openclaw hooks enable self-improvement
```
See `references/openclaw-integration.md` for complete details.
---
## Generic Setup (Other Agents)
For Claude Code, Codex, Copilot, or other agents, create `.learnings/` in your project:
```bash
mkdir -p .learnings
```
Copy templates from `assets/` or create files with headers.
### Add reference to agent files AGENTS.md, CLAUDE.md, or .github/copilot-instructions.md to remind yourself to log learnings. (this is an alternative to hook-based reminders)
#### Self-Improvement Workflow
When errors or corrections occur:
1. Log to `.learnings/ERRORS.md`, `LEARNINGS.md`, or `FEATURE_REQUESTS.md`
2. Review and promote broadly applicable learnings to:
- `CLAUDE.md` - project facts and conventions
- `AGENTS.md` - workflows and automation
- `.github/copilot-instructions.md` - Copilot context
## Logging Format
### Learning Entry
Append to `.learnings/LEARNINGS.md`:
```markdown
## [LRN-YYYYMMDD-XXX] category
**Logged**: ISO-8601 timestamp
**Priority**: low | medium | high | critical
**Status**: pending
**Area**: frontend | backend | infra | tests | docs | config
### Summary
One-line description of what was learned
### Details
Full context: what happened, what was wrong, what's correct
### Suggested Action
Specific fix or improvement to make
### Metadata
- Source: conversation | error | user_feedback
- Related Files: path/to/file.ext
- Tags: tag1, tag2
- See Also: LRN-20250110-001 (if related to existing entry)
- Pattern-Key: simplify.dead_code | harden.input_validation (optional, for recurring-pattern tracking)
- Recurrence-Count: 1 (optional)
- First-Seen: 2025-01-15 (optional)
- Last-Seen: 2025-01-15 (optional)
---
```
### Error Entry
Append to `.learnings/ERRORS.md`:
```markdown
## [ERR-YYYYMMDD-XXX] skill_or_command_name
**Logged**: ISO-8601 timestamp
**Priority**: high
**Status**: pending
**Area**: frontend | backend | infra | tests | docs | config
### Summary
Brief description of what failed
### Error
```
Actual error message or output
```
### Context
- Command/operation attempted
- Input or parameters used
- Environment details if relevant
### Suggested Fix
If identifiable, what might resolve this
### Metadata
- Reproducible: yes | no | unknown
- Related Files: path/to/file.ext
- See Also: ERR-20250110-001 (if recurring)
---
```
### Feature Request Entry
Append to `.learnings/FEATURE_REQUESTS.md`:
```markdown
## [FEAT-YYYYMMDD-XXX] capability_name
**Logged**: ISO-8601 timestamp
**Priority**: medium
**Status**: pending
**Area**: frontend | backend | infra | tests | docs | config
### Requested Capability
What the user wanted to do
### User Context
Why they needed it, what problem they're solving
### Complexity Estimate
simple | medium | complex
### Suggested Implementation
How this could be built, what it might extend
### Metadata
- Frequency: first_time | recurring
- Related Features: existing_feature_name
---
```
## ID Generation
Format: `TYPE-YYYYMMDD-XXX`
- TYPE: `LRN` (learning), `ERR` (error), `FEAT` (feature)
- YYYYMMDD: Current date
- XXX: Sequential number or random 3 chars (e.g., `001`, `A7B`)
Examples: `LRN-20250115-001`, `ERR-20250115-A3F`, `FEAT-20250115-002`
## Resolving Entries
When an issue is fixed, update the entry:
1. Change `**Status**: pending` → `**Status**: resolved`
2. Add resolution block after Metadata:
```markdown
### Resolution
- **Resolved**: 2025-01-16T09:00:00Z
- **Commit/PR**: abc123 or #42
- **Notes**: Brief description of what was done
```
Other status values:
- `in_progress` - Actively being worked on
- `wont_fix` - Decided not to address (add reason in Resolution notes)
- `promoted` - Elevated to CLAUDE.md, AGENTS.md, or .github/copilot-instructions.md
## Promoting to Project Memory
When a learning is broadly applicable (not a one-off fix), promote it to permanent project memory.
### When to Promote
- Learning applies across multiple files/features
- Knowledge any contributor (human or AI) should know
- Prevents recurring mistakes
- Documents project-specific conventions
### Promotion Targets
| Target | What Belongs There |
|--------|-------------------|
| `CLAUDE.md` | Project facts, conventions, gotchas for all Claude interactions |
| `AGENTS.md` | Agent-specific workflows, tool usage patterns, automation rules |
| `.github/copilot-instructions.md` | Project context and conventions for GitHub Copilot |
| `SOUL.md` | Behavioral guidelines, communication style, principles (OpenClaw workspace) |
| `TOOLS.md` | Tool capabilities, usage patterns, integration gotchas (OpenClaw workspace) |
### How to Promote
1. **Distill** the learning into a concise rule or fact
2. **Add** to appropriate section in target file (create file if needed)
3. **Update** original entry:
- Change `**Status**: pending` → `**Status**: promoted`
- Add `**Promoted**: CLAUDE.md`, `AGENTS.md`, or `.github/copilot-instructions.md`
### Promotion Examples
**Learning** (verbose):
> Project uses pnpm workspaces. Attempted `npm install` but failed.
> Lock file is `pnpm-lock.yaml`. Must use `pnpm install`.
**In CLAUDE.md** (concise):
```markdown
## Build & Dependencies
- Package manager: pnpm (not npm) - use `pnpm install`
```
**Learning** (verbose):
> When modifying API endpoints, must regenerate TypeScript client.
> Forgetting this causes type mismatches at runtime.
**In AGENTS.md** (actionable):
```markdown
## After API Changes
1. Regenerate client: `pnpm run generate:api`
2. Check for type errors: `pnpm tsc --noEmit`
```
## Recurring Pattern Detection
If logging something similar to an existing entry:
1. **Search first**: `grep -r "keyword" .learnings/`
2. **Link entries**: Add `**See Also**: ERR-20250110-001` in Metadata
3. **Bump priority** if issue keeps recurring
4. **Consider systemic fix**: Recurring issues often indicate:
- Missing documentation (→ promote to CLAUDE.md or .github/copilot-instructions.md)
- Missing automation (→ add to AGENTS.md)
- Architectural problem (→ create tech debt ticket)
## Simplify & Harden Feed
Use this workflow to ingest recurring patterns from the `simplify-and-harden`
skill and turn them into durable prompt guidance.
### Ingestion Workflow
1. Read `simplify_and_harden.learning_loop.candidates` from the task summary.
2. For each candidate, use `pattern_key` as the stable dedupe key.
3. Search `.learnings/LEARNINGS.md` for an existing entry with that key:
- `grep -n "Pattern-Key: <pattern_key>" .learnings/LEARNINGS.md`
4. If found:
- Increment `Recurrence-Count`
- Update `Last-Seen`
- Add `See Also` links to related entries/tasks
5. If not found:
- Create a new `LRN-...` entry
- Set `Source: simplify-and-harden`
- Set `Pattern-Key`, `Recurrence-Count: 1`, and `First-Seen`/`Last-Seen`
### Promotion Rule (System Prompt Feedback)
Promote recurring patterns into agent context/system prompt files when all are true:
- `Recurrence-Count >= 3`
- Seen across at least 2 distinct tasks
- Occurred within a 30-day window
Promotion targets:
- `CLAUDE.md`
- `AGENTS.md`
- `.github/copilot-instructions.md`
- `SOUL.md` / `TOOLS.md` for OpenClaw workspace-level guidance when applicable
Write promoted rules as short prevention rules (what to do before/while coding),
not long incident write-ups.
## Periodic Review
Review `.learnings/` at natural breakpoints:
### When to Review
- Before starting a new major task
- After completing a feature
- When working in an area with past learnings
- Weekly during active development
### Quick Status Check
```bash
# Count pending items
grep -h "Status\*\*: pending" .learnings/*.md | wc -l
# List pending high-priority items
grep -B5 "Priority\*\*: high" .learnings/*.md | grep "^## \["
# Find learnings for a specific area
grep -l "Area\*\*: backend" .learnings/*.md
```
### Review Actions
- Resolve fixed items
- Promote applicable learnings
- Link related entries
- Escalate recurring issues
## Detection Triggers
Automatically log when you notice:
**Corrections** (→ learning with `correction` category):
- "No, that's not right..."
- "Actually, it should be..."
- "You're wrong about..."
- "That's outdated..."
**Feature Requests** (→ feature request):
- "Can you also..."
- "I wish you could..."
- "Is there a way to..."
- "Why can't you..."
**Knowledge Gaps** (→ learning with `knowledge_gap` category):
- User provides information you didn't know
- Documentation you referenced is outdated
- API behavior differs from your understanding
**Errors** (→ error entry):
- Command returns non-zero exit code
- Exception or stack trace
- Unexpected output or behavior
- Timeout or connection failure
## Priority Guidelines
| Priority | When to Use |
|----------|-------------|
| `critical` | Blocks core functionality, data loss risk, security issue |
| `high` | Significant impact, affects common workflows, recurring issue |
| `medium` | Moderate impact, workaround exists |
| `low` | Minor inconvenience, edge case, nice-to-have |
## Area Tags
Use to filter learnings by codebase region:
| Area | Scope |
|------|-------|
| `frontend` | UI, components, client-side code |
| `backend` | API, services, server-side code |
| `infra` | CI/CD, deployment, Docker, cloud |
| `tests` | Test files, testing utilities, coverage |
| `docs` | Documentation, comments, READMEs |
| `config` | Configuration files, environment, settings |
## Best Practices
1. **Log immediately** - context is freshest right after the issue
2. **Be specific** - future agents need to understand quickly
3. **Include reproduction steps** - especially for errors
4. **Link related files** - makes fixes easier
5. **Suggest concrete fixes** - not just "investigate"
6. **Use consistent categories** - enables filtering
7. **Promote aggressively** - if in doubt, add to CLAUDE.md or .github/copilot-instructions.md
8. **Review regularly** - stale learnings lose value
## Gitignore Options
**Keep learnings local** (per-developer):
```gitignore
.learnings/
```
**Track learnings in repo** (team-wide):
Don't add to .gitignore - learnings become shared knowledge.
**Hybrid** (track templates, ignore entries):
```gitignore
.learnings/*.md
!.learnings/.gitkeep
```
## Hook Integration
Enable automatic reminders through agent hooks. This is **opt-in** - you must explicitly configure hooks.
### Quick Setup (Claude Code / Codex)
Create `.claude/settings.json` in your project:
```json
{
"hooks": {
"UserPromptSubmit": [{
"matcher": "",
"hooks": [{
"type": "command",
"command": "./skills/self-improvement/scripts/activator.sh"
}]
}]
}
}
```
This injects a learning evaluation reminder after each prompt (~50-100 tokens overhead).
### Full Setup (With Error Detection)
```json
{
"hooks": {
"UserPromptSubmit": [{
"matcher": "",
"hooks": [{
"type": "command",
"command": "./skills/self-improvement/scripts/activator.sh"
}]
}],
"PostToolUse": [{
"matcher": "Bash",
"hooks": [{
"type": "command",
"command": "./skills/self-improvement/scripts/error-detector.sh"
}]
}]
}
}
```
### Available Hook Scripts
| Script | Hook Type | Purpose |
|--------|-----------|---------|
| `scripts/activator.sh` | UserPromptSubmit | Reminds to evaluate learnings after tasks |
| `scripts/error-detector.sh` | PostToolUse (Bash) | Triggers on command errors |
See `references/hooks-setup.md` for detailed configuration and troubleshooting.
## Automatic Skill Extraction
When a learning is valuable enough to become a reusable skill, extract it using the provided helper.
### Skill Extraction Criteria
A learning qualifies for skill extraction when ANY of these apply:
| Criterion | Description |
|-----------|-------------|
| **Recurring** | Has `See Also` links to 2+ similar issues |
| **Verified** | Status is `resolved` with working fix |
| **Non-obvious** | Required actual debugging/investigation to discover |
| **Broadly applicable** | Not project-specific; useful across codebases |
| **User-flagged** | User says "save this as a skill" or similar |
### Extraction Workflow
1. **Identify candidate**: Learning meets extraction criteria
2. **Run helper** (or create manually):
```bash
./skills/self-improvement/scripts/extract-skill.sh skill-name --dry-run
./skills/self-improvement/scripts/extract-skill.sh skill-name
```
3. **Customize SKILL.md**: Fill in template with learning content
4. **Update learning**: Set status to `promoted_to_skill`, add `Skill-Path`
5. **Verify**: Read skill in fresh session to ensure it's self-contained
### Manual Extraction
If you prefer manual creation:
1. Create `skills/<skill-name>/SKILL.md`
2. Use template from `assets/SKILL-TEMPLATE.md`
3. Follow [Agent Skills spec](https://agentskills.io/specification):
- YAML frontmatter with `name` and `description`
- Name must match folder name
- No README.md inside skill folder
### Extraction Detection Triggers
Watch for these signals that a learning should become a skill:
**In conversation:**
- "Save this as a skill"
- "I keep running into this"
- "This would be useful for other projects"
- "Remember this pattern"
**In learning entries:**
- Multiple `See Also` links (recurring issue)
- High priority + resolved status
- Category: `best_practice` with broad applicability
- User feedback praising the solution
### Skill Quality Gates
Before extraction, verify:
- [ ] Solution is tested and working
- [ ] Description is clear without original context
- [ ] Code examples are self-contained
- [ ] No project-specific hardcoded values
- [ ] Follows skill naming conventions (lowercase, hyphens)
## Multi-Agent Support
This skill works across different AI coding agents with agent-specific activation.
### Claude Code
**Activation**: Hooks (UserPromptSubmit, PostToolUse)
**Setup**: `.claude/settings.json` with hook configuration
**Detection**: Automatic via hook scripts
### Codex CLI
**Activation**: Hooks (same pattern as Claude Code)
**Setup**: `.codex/settings.json` with hook configuration
**Detection**: Automatic via hook scripts
### GitHub Copilot
**Activation**: Manual (no hook support)
**Setup**: Add to `.github/copilot-instructions.md`:
```markdown
## Self-Improvement
After solving non-obvious issues, consider logging to `.learnings/`:
1. Use format from self-improvement skill
2. Link related entries with See Also
3. Promote high-value learnings to skills
Ask in chat: "Should I log this as a learning?"
```
**Detection**: Manual review at session end
### OpenClaw
**Activation**: Workspace injection + inter-agent messaging
**Setup**: See "OpenClaw Setup" section above
**Detection**: Via session tools and workspace files
### Agent-Agnostic Guidance
Regardless of agent, apply self-improvement when you:
1. **Discover something non-obvious** - solution wasn't immediate
2. **Correct yourself** - initial approach was wrong
3. **Learn project conventions** - discovered undocumented patterns
4. **Hit unexpected errors** - especially if diagnosis was difficult
5. **Find better approaches** - improved on your original solution
### Copilot Chat Integration
For Copilot users, add this to your prompts when relevant:
> After completing this task, evaluate if any learnings should be logged to `.learnings/` using the self-improvement skill format.
Or use quick prompts:
- "Log this to learnings"
- "Create a skill from this solution"
- "Check .learnings/ for related issues"
FILE:_meta.json
{
"ownerId": "kn70cjr952qdec1nx70zs6wefn7ynq2t",
"slug": "lovefromio-self-improving-agent",
"version": "3.0.6",
"publishedAt": 1774365304323
}
FILE:assets/LEARNINGS.md
# Learnings
Corrections, insights, and knowledge gaps captured during development.
**Categories**: correction | insight | knowledge_gap | best_practice
**Areas**: frontend | backend | infra | tests | docs | config
**Statuses**: pending | in_progress | resolved | wont_fix | promoted | promoted_to_skill
## Status Definitions
| Status | Meaning |
|--------|---------|
| `pending` | Not yet addressed |
| `in_progress` | Actively being worked on |
| `resolved` | Issue fixed or knowledge integrated |
| `wont_fix` | Decided not to address (reason in Resolution) |
| `promoted` | Elevated to CLAUDE.md, AGENTS.md, or copilot-instructions.md |
| `promoted_to_skill` | Extracted as a reusable skill |
## Skill Extraction Fields
When a learning is promoted to a skill, add these fields:
```markdown
**Status**: promoted_to_skill
**Skill-Path**: skills/skill-name
```
Example:
```markdown
## [LRN-20250115-001] best_practice
**Logged**: 2025-01-15T10:00:00Z
**Priority**: high
**Status**: promoted_to_skill
**Skill-Path**: skills/docker-m1-fixes
**Area**: infra
### Summary
Docker build fails on Apple Silicon due to platform mismatch
...
```
---
FILE:assets/SKILL-TEMPLATE.md
# Skill Template
Template for creating skills extracted from learnings. Copy and customize.
---
## SKILL.md Template
```markdown
---
name: skill-name-here
description: "Concise description of when and why to use this skill. Include trigger conditions."
---
# Skill Name
Brief introduction explaining the problem this skill solves and its origin.
## Quick Reference
| Situation | Action |
|-----------|--------|
| [Trigger 1] | [Action 1] |
| [Trigger 2] | [Action 2] |
## Background
Why this knowledge matters. What problems it prevents. Context from the original learning.
## Solution
### Step-by-Step
1. First step with code or command
2. Second step
3. Verification step
### Code Example
\`\`\`language
// Example code demonstrating the solution
\`\`\`
## Common Variations
- **Variation A**: Description and how to handle
- **Variation B**: Description and how to handle
## Gotchas
- Warning or common mistake #1
- Warning or common mistake #2
## Related
- Link to related documentation
- Link to related skill
## Source
Extracted from learning entry.
- **Learning ID**: LRN-YYYYMMDD-XXX
- **Original Category**: correction | insight | knowledge_gap | best_practice
- **Extraction Date**: YYYY-MM-DD
```
---
## Minimal Template
For simple skills that don't need all sections:
```markdown
---
name: skill-name-here
description: "What this skill does and when to use it."
---
# Skill Name
[Problem statement in one sentence]
## Solution
[Direct solution with code/commands]
## Source
- Learning ID: LRN-YYYYMMDD-XXX
```
---
## Template with Scripts
For skills that include executable helpers:
```markdown
---
name: skill-name-here
description: "What this skill does and when to use it."
---
# Skill Name
[Introduction]
## Quick Reference
| Command | Purpose |
|---------|---------|
| `./scripts/helper.sh` | [What it does] |
| `./scripts/validate.sh` | [What it does] |
## Usage
### Automated (Recommended)
\`\`\`bash
./skills/skill-name/scripts/helper.sh [args]
\`\`\`
### Manual Steps
1. Step one
2. Step two
## Scripts
| Script | Description |
|--------|-------------|
| `scripts/helper.sh` | Main utility |
| `scripts/validate.sh` | Validation checker |
## Source
- Learning ID: LRN-YYYYMMDD-XXX
```
---
## Naming Conventions
- **Skill name**: lowercase, hyphens for spaces
- Good: `docker-m1-fixes`, `api-timeout-patterns`
- Bad: `Docker_M1_Fixes`, `APITimeoutPatterns`
- **Description**: Start with action verb, mention trigger
- Good: "Handles Docker build failures on Apple Silicon. Use when builds fail with platform mismatch."
- Bad: "Docker stuff"
- **Files**:
- `SKILL.md` - Required, main documentation
- `scripts/` - Optional, executable code
- `references/` - Optional, detailed docs
- `assets/` - Optional, templates
---
## Extraction Checklist
Before creating a skill from a learning:
- [ ] Learning is verified (status: resolved)
- [ ] Solution is broadly applicable (not one-off)
- [ ] Content is complete (has all needed context)
- [ ] Name follows conventions
- [ ] Description is concise but informative
- [ ] Quick Reference table is actionable
- [ ] Code examples are tested
- [ ] Source learning ID is recorded
After creating:
- [ ] Update original learning with `promoted_to_skill` status
- [ ] Add `Skill-Path: skills/skill-name` to learning metadata
- [ ] Test skill by reading it in a fresh session
FILE:hooks/openclaw/HOOK.md
---
name: self-improvement
description: "Injects self-improvement reminder during agent bootstrap"
metadata: {"openclaw":{"emoji":"🧠","events":["agent:bootstrap"]}}
---
# Self-Improvement Hook
Injects a reminder to evaluate learnings during agent bootstrap.
## What It Does
- Fires on `agent:bootstrap` (before workspace files are injected)
- Adds a reminder block to check `.learnings/` for relevant entries
- Prompts the agent to log corrections, errors, and discoveries
## Configuration
No configuration needed. Enable with:
```bash
openclaw hooks enable self-improvement
```
FILE:hooks/openclaw/handler.js
/**
* Self-Improvement Hook for OpenClaw
*
* Injects a reminder to evaluate learnings during agent bootstrap.
* Fires on agent:bootstrap event before workspace files are injected.
*/
const REMINDER_CONTENT = `
## Self-Improvement Reminder
After completing tasks, evaluate if any learnings should be captured:
**Log when:**
- User corrects you → \`.learnings/LEARNINGS.md\`
- Command/operation fails → \`.learnings/ERRORS.md\`
- User wants missing capability → \`.learnings/FEATURE_REQUESTS.md\`
- You discover your knowledge was wrong → \`.learnings/LEARNINGS.md\`
- You find a better approach → \`.learnings/LEARNINGS.md\`
**Promote when pattern is proven:**
- Behavioral patterns → \`SOUL.md\`
- Workflow improvements → \`AGENTS.md\`
- Tool gotchas → \`TOOLS.md\`
Keep entries simple: date, title, what happened, what to do differently.
`.trim();
const handler = async (event) => {
// Safety checks for event structure
if (!event || typeof event !== 'object') {
return;
}
// Only handle agent:bootstrap events
if (event.type !== 'agent' || event.action !== 'bootstrap') {
return;
}
// Safety check for context
if (!event.context || typeof event.context !== 'object') {
return;
}
// Inject the reminder as a virtual bootstrap file
// Check that bootstrapFiles is an array before pushing
if (Array.isArray(event.context.bootstrapFiles)) {
event.context.bootstrapFiles.push({
path: 'SELF_IMPROVEMENT_REMINDER.md',
content: REMINDER_CONTENT,
virtual: true,
});
}
};
module.exports = handler;
module.exports.default = handler;
FILE:hooks/openclaw/handler.ts
/**
* Self-Improvement Hook for OpenClaw
*
* Injects a reminder to evaluate learnings during agent bootstrap.
* Fires on agent:bootstrap event before workspace files are injected.
*/
import type { HookHandler } from 'openclaw/hooks';
const REMINDER_CONTENT = `## Self-Improvement Reminder
After completing tasks, evaluate if any learnings should be captured:
**Log when:**
- User corrects you → \`.learnings/LEARNINGS.md\`
- Command/operation fails → \`.learnings/ERRORS.md\`
- User wants missing capability → \`.learnings/FEATURE_REQUESTS.md\`
- You discover your knowledge was wrong → \`.learnings/LEARNINGS.md\`
- You find a better approach → \`.learnings/LEARNINGS.md\`
**Promote when pattern is proven:**
- Behavioral patterns → \`SOUL.md\`
- Workflow improvements → \`AGENTS.md\`
- Tool gotchas → \`TOOLS.md\`
Keep entries simple: date, title, what happened, what to do differently.`;
const handler: HookHandler = async (event) => {
// Safety checks for event structure
if (!event || typeof event !== 'object') {
return;
}
// Only handle agent:bootstrap events
if (event.type !== 'agent' || event.action !== 'bootstrap') {
return;
}
// Safety check for context
if (!event.context || typeof event.context !== 'object') {
return;
}
// Skip sub-agent sessions to avoid bootstrap issues
// Sub-agents have sessionKey patterns like "agent:main:subagent:..."
const sessionKey = event.sessionKey || '';
if (sessionKey.includes(':subagent:')) {
return;
}
// Inject the reminder as a virtual bootstrap file
// Check that bootstrapFiles is an array before pushing
if (Array.isArray(event.context.bootstrapFiles)) {
event.context.bootstrapFiles.push({
path: 'SELF_IMPROVEMENT_REMINDER.md',
content: REMINDER_CONTENT,
virtual: true,
});
}
};
export default handler;
FILE:references/examples.md
# Entry Examples
Concrete examples of well-formatted entries with all fields.
## Learning: Correction
```markdown
## [LRN-20250115-001] correction
**Logged**: 2025-01-15T10:30:00Z
**Priority**: high
**Status**: pending
**Area**: tests
### Summary
Incorrectly assumed pytest fixtures are scoped to function by default
### Details
When writing test fixtures, I assumed all fixtures were function-scoped.
User corrected that while function scope is the default, the codebase
convention uses module-scoped fixtures for database connections to
improve test performance.
### Suggested Action
When creating fixtures that involve expensive setup (DB, network),
check existing fixtures for scope patterns before defaulting to function scope.
### Metadata
- Source: user_feedback
- Related Files: tests/conftest.py
- Tags: pytest, testing, fixtures
---
```
## Learning: Knowledge Gap (Resolved)
```markdown
## [LRN-20250115-002] knowledge_gap
**Logged**: 2025-01-15T14:22:00Z
**Priority**: medium
**Status**: resolved
**Area**: config
### Summary
Project uses pnpm not npm for package management
### Details
Attempted to run `npm install` but project uses pnpm workspaces.
Lock file is `pnpm-lock.yaml`, not `package-lock.json`.
### Suggested Action
Check for `pnpm-lock.yaml` or `pnpm-workspace.yaml` before assuming npm.
Use `pnpm install` for this project.
### Metadata
- Source: error
- Related Files: pnpm-lock.yaml, pnpm-workspace.yaml
- Tags: package-manager, pnpm, setup
### Resolution
- **Resolved**: 2025-01-15T14:30:00Z
- **Commit/PR**: N/A - knowledge update
- **Notes**: Added to CLAUDE.md for future reference
---
```
## Learning: Promoted to CLAUDE.md
```markdown
## [LRN-20250115-003] best_practice
**Logged**: 2025-01-15T16:00:00Z
**Priority**: high
**Status**: promoted
**Promoted**: CLAUDE.md
**Area**: backend
### Summary
API responses must include correlation ID from request headers
### Details
All API responses should echo back the X-Correlation-ID header from
the request. This is required for distributed tracing. Responses
without this header break the observability pipeline.
### Suggested Action
Always include correlation ID passthrough in API handlers.
### Metadata
- Source: user_feedback
- Related Files: src/middleware/correlation.ts
- Tags: api, observability, tracing
---
```
## Learning: Promoted to AGENTS.md
```markdown
## [LRN-20250116-001] best_practice
**Logged**: 2025-01-16T09:00:00Z
**Priority**: high
**Status**: promoted
**Promoted**: AGENTS.md
**Area**: backend
### Summary
Must regenerate API client after OpenAPI spec changes
### Details
When modifying API endpoints, the TypeScript client must be regenerated.
Forgetting this causes type mismatches that only appear at runtime.
The generate script also runs validation.
### Suggested Action
Add to agent workflow: after any API changes, run `pnpm run generate:api`.
### Metadata
- Source: error
- Related Files: openapi.yaml, src/client/api.ts
- Tags: api, codegen, typescript
---
```
## Error Entry
```markdown
## [ERR-20250115-A3F] docker_build
**Logged**: 2025-01-15T09:15:00Z
**Priority**: high
**Status**: pending
**Area**: infra
### Summary
Docker build fails on M1 Mac due to platform mismatch
### Error
```
error: failed to solve: python:3.11-slim: no match for platform linux/arm64
```
### Context
- Command: `docker build -t myapp .`
- Dockerfile uses `FROM python:3.11-slim`
- Running on Apple Silicon (M1/M2)
### Suggested Fix
Add platform flag: `docker build --platform linux/amd64 -t myapp .`
Or update Dockerfile: `FROM --platform=linux/amd64 python:3.11-slim`
### Metadata
- Reproducible: yes
- Related Files: Dockerfile
---
```
## Error Entry: Recurring Issue
```markdown
## [ERR-20250120-B2C] api_timeout
**Logged**: 2025-01-20T11:30:00Z
**Priority**: critical
**Status**: pending
**Area**: backend
### Summary
Third-party payment API timeout during checkout
### Error
```
TimeoutError: Request to payments.example.com timed out after 30000ms
```
### Context
- Command: POST /api/checkout
- Timeout set to 30s
- Occurs during peak hours (lunch, evening)
### Suggested Fix
Implement retry with exponential backoff. Consider circuit breaker pattern.
### Metadata
- Reproducible: yes (during peak hours)
- Related Files: src/services/payment.ts
- See Also: ERR-20250115-X1Y, ERR-20250118-Z3W
---
```
## Feature Request
```markdown
## [FEAT-20250115-001] export_to_csv
**Logged**: 2025-01-15T16:45:00Z
**Priority**: medium
**Status**: pending
**Area**: backend
### Requested Capability
Export analysis results to CSV format
### User Context
User runs weekly reports and needs to share results with non-technical
stakeholders in Excel. Currently copies output manually.
### Complexity Estimate
simple
### Suggested Implementation
Add `--output csv` flag to the analyze command. Use standard csv module.
Could extend existing `--output json` pattern.
### Metadata
- Frequency: recurring
- Related Features: analyze command, json output
---
```
## Feature Request: Resolved
```markdown
## [FEAT-20250110-002] dark_mode
**Logged**: 2025-01-10T14:00:00Z
**Priority**: low
**Status**: resolved
**Area**: frontend
### Requested Capability
Dark mode support for the dashboard
### User Context
User works late hours and finds the bright interface straining.
Several other users have mentioned this informally.
### Complexity Estimate
medium
### Suggested Implementation
Use CSS variables for colors. Add toggle in user settings.
Consider system preference detection.
### Metadata
- Frequency: recurring
- Related Features: user settings, theme system
### Resolution
- **Resolved**: 2025-01-18T16:00:00Z
- **Commit/PR**: #142
- **Notes**: Implemented with system preference detection and manual toggle
---
```
## Learning: Promoted to Skill
```markdown
## [LRN-20250118-001] best_practice
**Logged**: 2025-01-18T11:00:00Z
**Priority**: high
**Status**: promoted_to_skill
**Skill-Path**: skills/docker-m1-fixes
**Area**: infra
### Summary
Docker build fails on Apple Silicon due to platform mismatch
### Details
When building Docker images on M1/M2 Macs, the build fails because
the base image doesn't have an ARM64 variant. This is a common issue
that affects many developers.
### Suggested Action
Add `--platform linux/amd64` to docker build command, or use
`FROM --platform=linux/amd64` in Dockerfile.
### Metadata
- Source: error
- Related Files: Dockerfile
- Tags: docker, arm64, m1, apple-silicon
- See Also: ERR-20250115-A3F, ERR-20250117-B2D
---
```
## Extracted Skill Example
When the above learning is extracted as a skill, it becomes:
**File**: `skills/docker-m1-fixes/SKILL.md`
```markdown
---
name: docker-m1-fixes
description: "Fixes Docker build failures on Apple Silicon (M1/M2). Use when docker build fails with platform mismatch errors."
---
# Docker M1 Fixes
Solutions for Docker build issues on Apple Silicon Macs.
## Quick Reference
| Error | Fix |
|-------|-----|
| `no match for platform linux/arm64` | Add `--platform linux/amd64` to build |
| Image runs but crashes | Use emulation or find ARM-compatible base |
## The Problem
Many Docker base images don't have ARM64 variants. When building on
Apple Silicon (M1/M2/M3), Docker attempts to pull ARM64 images by
default, causing platform mismatch errors.
## Solutions
### Option 1: Build Flag (Recommended)
Add platform flag to your build command:
\`\`\`bash
docker build --platform linux/amd64 -t myapp .
\`\`\`
### Option 2: Dockerfile Modification
Specify platform in the FROM instruction:
\`\`\`dockerfile
FROM --platform=linux/amd64 python:3.11-slim
\`\`\`
### Option 3: Docker Compose
Add platform to your service:
\`\`\`yaml
services:
app:
platform: linux/amd64
build: .
\`\`\`
## Trade-offs
| Approach | Pros | Cons |
|----------|------|------|
| Build flag | No file changes | Must remember flag |
| Dockerfile | Explicit, versioned | Affects all builds |
| Compose | Convenient for dev | Requires compose |
## Performance Note
Running AMD64 images on ARM64 uses Rosetta 2 emulation. This works
for development but may be slower. For production, find ARM-native
alternatives when possible.
## Source
- Learning ID: LRN-20250118-001
- Category: best_practice
- Extraction Date: 2025-01-18
```
FILE:references/hooks-setup.md
# Hook Setup Guide
Configure automatic self-improvement triggers for AI coding agents.
## Overview
Hooks enable proactive learning capture by injecting reminders at key moments:
- **UserPromptSubmit**: Reminder after each prompt to evaluate learnings
- **PostToolUse (Bash)**: Error detection when commands fail
## Claude Code Setup
### Option 1: Project-Level Configuration
Create `.claude/settings.json` in your project root:
```json
{
"hooks": {
"UserPromptSubmit": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "./skills/self-improvement/scripts/activator.sh"
}
]
}
],
"PostToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "./skills/self-improvement/scripts/error-detector.sh"
}
]
}
]
}
}
```
### Option 2: User-Level Configuration
Add to `~/.claude/settings.json` for global activation:
```json
{
"hooks": {
"UserPromptSubmit": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "~/.claude/skills/self-improvement/scripts/activator.sh"
}
]
}
]
}
}
```
### Minimal Setup (Activator Only)
For lower overhead, use only the UserPromptSubmit hook:
```json
{
"hooks": {
"UserPromptSubmit": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "./skills/self-improvement/scripts/activator.sh"
}
]
}
]
}
}
```
## Codex CLI Setup
Codex uses the same hook system as Claude Code. Create `.codex/settings.json`:
```json
{
"hooks": {
"UserPromptSubmit": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "./skills/self-improvement/scripts/activator.sh"
}
]
}
]
}
}
```
## GitHub Copilot Setup
Copilot doesn't support hooks directly. Instead, add guidance to `.github/copilot-instructions.md`:
```markdown
## Self-Improvement
After completing tasks that involved:
- Debugging non-obvious issues
- Discovering workarounds
- Learning project-specific patterns
- Resolving unexpected errors
Consider logging the learning to `.learnings/` using the format from the self-improvement skill.
For high-value learnings that would benefit other sessions, consider skill extraction.
```
## Verification
### Test Activator Hook
1. Enable the hook configuration
2. Start a new Claude Code session
3. Send any prompt
4. Verify you see `<self-improvement-reminder>` in the context
### Test Error Detector Hook
1. Enable PostToolUse hook for Bash
2. Run a command that fails: `ls /nonexistent/path`
3. Verify you see `<error-detected>` reminder
### Dry Run Extract Script
```bash
./skills/self-improvement/scripts/extract-skill.sh test-skill --dry-run
```
Expected output shows the skill scaffold that would be created.
## Troubleshooting
### Hook Not Triggering
1. **Check script permissions**: `chmod +x scripts/*.sh`
2. **Verify path**: Use absolute paths or paths relative to project root
3. **Check settings location**: Project vs user-level settings
4. **Restart session**: Hooks are loaded at session start
### Permission Denied
```bash
chmod +x ./skills/self-improvement/scripts/activator.sh
chmod +x ./skills/self-improvement/scripts/error-detector.sh
chmod +x ./skills/self-improvement/scripts/extract-skill.sh
```
### Script Not Found
If using relative paths, ensure you're in the correct directory or use absolute paths:
```json
{
"command": "/absolute/path/to/skills/self-improvement/scripts/activator.sh"
}
```
### Too Much Overhead
If the activator feels intrusive:
1. **Use minimal setup**: Only UserPromptSubmit, skip PostToolUse
2. **Add matcher filter**: Only trigger for certain prompts:
```json
{
"matcher": "fix|debug|error|issue",
"hooks": [...]
}
```
## Hook Output Budget
The activator is designed to be lightweight:
- **Target**: ~50-100 tokens per activation
- **Content**: Structured reminder, not verbose instructions
- **Format**: XML tags for easy parsing
If you need to reduce overhead further, you can edit `activator.sh` to output less text.
## Security Considerations
- Hook scripts run with the same permissions as Claude Code
- Scripts only output text; they don't modify files or run commands
- Error detector reads `CLAUDE_TOOL_OUTPUT` environment variable
- All scripts are opt-in (you must configure them explicitly)
## Disabling Hooks
To temporarily disable without removing configuration:
1. **Comment out in settings**:
```json
{
"hooks": {
// "UserPromptSubmit": [...]
}
}
```
2. **Or delete the settings file**: Hooks won't run without configuration
FILE:references/openclaw-integration.md
# OpenClaw Integration
Complete setup and usage guide for integrating the self-improvement skill with OpenClaw.
## Overview
OpenClaw uses workspace-based prompt injection combined with event-driven hooks. Context is injected from workspace files at session start, and hooks can trigger on lifecycle events.
## Workspace Structure
```
~/.openclaw/
├── workspace/ # Working directory
│ ├── AGENTS.md # Multi-agent coordination patterns
│ ├── SOUL.md # Behavioral guidelines and personality
│ ├── TOOLS.md # Tool capabilities and gotchas
│ ├── MEMORY.md # Long-term memory (main session only)
│ └── memory/ # Daily memory files
│ └── YYYY-MM-DD.md
├── skills/ # Installed skills
│ └── <skill-name>/
│ └── SKILL.md
└── hooks/ # Custom hooks
└── <hook-name>/
├── HOOK.md
└── handler.ts
```
## Quick Setup
### 1. Install the Skill
```bash
clawdhub install self-improving-agent
```
Or copy manually:
```bash
cp -r self-improving-agent ~/.openclaw/skills/
```
### 2. Install the Hook (Optional)
Copy the hook to OpenClaw's hooks directory:
```bash
cp -r hooks/openclaw ~/.openclaw/hooks/self-improvement
```
Enable the hook:
```bash
openclaw hooks enable self-improvement
```
### 3. Create Learning Files
Create the `.learnings/` directory in your workspace:
```bash
mkdir -p ~/.openclaw/workspace/.learnings
```
Or in the skill directory:
```bash
mkdir -p ~/.openclaw/skills/self-improving-agent/.learnings
```
## Injected Prompt Files
### AGENTS.md
Purpose: Multi-agent workflows and delegation patterns.
```markdown
# Agent Coordination
## Delegation Rules
- Use explore agent for open-ended codebase questions
- Spawn sub-agents for long-running tasks
- Use sessions_send for cross-session communication
## Session Handoff
When delegating to another session:
1. Provide full context in the handoff message
2. Include relevant file paths
3. Specify expected output format
```
### SOUL.md
Purpose: Behavioral guidelines and communication style.
```markdown
# Behavioral Guidelines
## Communication Style
- Be direct and concise
- Avoid unnecessary caveats and disclaimers
- Use technical language appropriate to context
## Error Handling
- Admit mistakes promptly
- Provide corrected information immediately
- Log significant errors to learnings
```
### TOOLS.md
Purpose: Tool capabilities, integration gotchas, local configuration.
```markdown
# Tool Knowledge
## Self-Improvement Skill
Log learnings to `.learnings/` for continuous improvement.
## Local Tools
- Document tool-specific gotchas here
- Note authentication requirements
- Track integration quirks
```
## Learning Workflow
### Capturing Learnings
1. **In-session**: Log to `.learnings/` as usual
2. **Cross-session**: Promote to workspace files
### Promotion Decision Tree
```
Is the learning project-specific?
├── Yes → Keep in .learnings/
└── No → Is it behavioral/style-related?
├── Yes → Promote to SOUL.md
└── No → Is it tool-related?
├── Yes → Promote to TOOLS.md
└── No → Promote to AGENTS.md (workflow)
```
### Promotion Format Examples
**From learning:**
> Git push to GitHub fails without auth configured - triggers desktop prompt
**To TOOLS.md:**
```markdown
## Git
- Don't push without confirming auth is configured
- Use `gh auth status` to check GitHub CLI auth
```
## Inter-Agent Communication
OpenClaw provides tools for cross-session communication:
### sessions_list
View active and recent sessions:
```
sessions_list(activeMinutes=30, messageLimit=3)
```
### sessions_history
Read transcript from another session:
```
sessions_history(sessionKey="session-id", limit=50)
```
### sessions_send
Send message to another session:
```
sessions_send(sessionKey="session-id", message="Learning: API requires X-Custom-Header")
```
### sessions_spawn
Spawn a background sub-agent:
```
sessions_spawn(task="Research X and report back", label="research")
```
## Available Hook Events
| Event | When It Fires |
|-------|---------------|
| `agent:bootstrap` | Before workspace files inject |
| `command:new` | When `/new` command issued |
| `command:reset` | When `/reset` command issued |
| `command:stop` | When `/stop` command issued |
| `gateway:startup` | When gateway starts |
## Detection Triggers
### Standard Triggers
- User corrections ("No, that's wrong...")
- Command failures (non-zero exit codes)
- API errors
- Knowledge gaps
### OpenClaw-Specific Triggers
| Trigger | Action |
|---------|--------|
| Tool call error | Log to TOOLS.md with tool name |
| Session handoff confusion | Log to AGENTS.md with delegation pattern |
| Model behavior surprise | Log to SOUL.md with expected vs actual |
| Skill issue | Log to .learnings/ or report upstream |
## Verification
Check hook is registered:
```bash
openclaw hooks list
```
Check skill is loaded:
```bash
openclaw status
```
## Troubleshooting
### Hook not firing
1. Ensure hooks enabled in config
2. Restart gateway after config changes
3. Check gateway logs for errors
### Learnings not persisting
1. Verify `.learnings/` directory exists
2. Check file permissions
3. Ensure workspace path is configured correctly
### Skill not loading
1. Check skill is in skills directory
2. Verify SKILL.md has correct frontmatter
3. Run `openclaw status` to see loaded skills
FILE:scripts/activator.sh
#!/bin/bash
# Self-Improvement Activator Hook
# Triggers on UserPromptSubmit to remind Claude about learning capture
# Keep output minimal (~50-100 tokens) to minimize overhead
set -e
# Output reminder as system context
cat << 'EOF'
<self-improvement-reminder>
After completing this task, evaluate if extractable knowledge emerged:
- Non-obvious solution discovered through investigation?
- Workaround for unexpected behavior?
- Project-specific pattern learned?
- Error required debugging to resolve?
If yes: Log to .learnings/ using the self-improvement skill format.
If high-value (recurring, broadly applicable): Consider skill extraction.
</self-improvement-reminder>
EOF
FILE:scripts/error-detector.sh
#!/bin/bash
# Self-Improvement Error Detector Hook
# Triggers on PostToolUse for Bash to detect command failures
# Reads CLAUDE_TOOL_OUTPUT environment variable
set -e
# Check if tool output indicates an error
# CLAUDE_TOOL_OUTPUT contains the result of the tool execution
OUTPUT="-"
# Patterns indicating errors (case-insensitive matching)
ERROR_PATTERNS=(
"error:"
"Error:"
"ERROR:"
"failed"
"FAILED"
"command not found"
"No such file"
"Permission denied"
"fatal:"
"Exception"
"Traceback"
"npm ERR!"
"ModuleNotFoundError"
"SyntaxError"
"TypeError"
"exit code"
"non-zero"
)
# Check if output contains any error pattern
contains_error=false
for pattern in "ERROR_PATTERNS[@]"; do
if [[ "$OUTPUT" == *"$pattern"* ]]; then
contains_error=true
break
fi
done
# Only output reminder if error detected
if [ "$contains_error" = true ]; then
cat << 'EOF'
<error-detected>
A command error was detected. Consider logging this to .learnings/ERRORS.md if:
- The error was unexpected or non-obvious
- It required investigation to resolve
- It might recur in similar contexts
- The solution could benefit future sessions
Use the self-improvement skill format: [ERR-YYYYMMDD-XXX]
</error-detected>
EOF
fi
FILE:scripts/extract-skill.sh
#!/bin/bash
# Skill Extraction Helper
# Creates a new skill from a learning entry
# Usage: ./extract-skill.sh <skill-name> [--dry-run]
set -e
# Configuration
SKILLS_DIR="./skills"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
usage() {
cat << EOF
Usage: $(basename "$0") <skill-name> [options]
Create a new skill from a learning entry.
Arguments:
skill-name Name of the skill (lowercase, hyphens for spaces)
Options:
--dry-run Show what would be created without creating files
--output-dir Relative output directory under current path (default: ./skills)
-h, --help Show this help message
Examples:
$(basename "$0") docker-m1-fixes
$(basename "$0") api-timeout-patterns --dry-run
$(basename "$0") pnpm-setup --output-dir ./skills/custom
The skill will be created in: \$SKILLS_DIR/<skill-name>/
EOF
}
log_info() {
echo -e "GREEN[INFO]NC $1"
}
log_warn() {
echo -e "YELLOW[WARN]NC $1"
}
log_error() {
echo -e "RED[ERROR]NC $1" >&2
}
# Parse arguments
SKILL_NAME=""
DRY_RUN=false
while [[ $# -gt 0 ]]; do
case $1 in
--dry-run)
DRY_RUN=true
shift
;;
--output-dir)
if [ -z "-" ] || [[ "-" == -* ]]; then
log_error "--output-dir requires a relative path argument"
usage
exit 1
fi
SKILLS_DIR="$2"
shift 2
;;
-h|--help)
usage
exit 0
;;
-*)
log_error "Unknown option: $1"
usage
exit 1
;;
*)
if [ -z "$SKILL_NAME" ]; then
SKILL_NAME="$1"
else
log_error "Unexpected argument: $1"
usage
exit 1
fi
shift
;;
esac
done
# Validate skill name
if [ -z "$SKILL_NAME" ]; then
log_error "Skill name is required"
usage
exit 1
fi
# Validate skill name format (lowercase, hyphens, no spaces)
if ! [[ "$SKILL_NAME" =~ ^[a-z0-9]+(-[a-z0-9]+)*$ ]]; then
log_error "Invalid skill name format. Use lowercase letters, numbers, and hyphens only."
log_error "Examples: 'docker-fixes', 'api-patterns', 'pnpm-setup'"
exit 1
fi
# Validate output path to avoid writes outside current workspace.
if [[ "$SKILLS_DIR" = /* ]]; then
log_error "Output directory must be a relative path under the current directory."
exit 1
fi
if [[ "$SKILLS_DIR" =~ (^|/)\.\.(/|$) ]]; then
log_error "Output directory cannot include '..' path segments."
exit 1
fi
SKILLS_DIR="SKILLS_DIR#./"
SKILLS_DIR="./$SKILLS_DIR"
SKILL_PATH="$SKILLS_DIR/$SKILL_NAME"
# Check if skill already exists
if [ -d "$SKILL_PATH" ] && [ "$DRY_RUN" = false ]; then
log_error "Skill already exists: $SKILL_PATH"
log_error "Use a different name or remove the existing skill first."
exit 1
fi
# Dry run output
if [ "$DRY_RUN" = true ]; then
log_info "Dry run - would create:"
echo " $SKILL_PATH/"
echo " $SKILL_PATH/SKILL.md"
echo ""
echo "Template content would be:"
echo "---"
cat << TEMPLATE
name: $SKILL_NAME
description: "[TODO: Add a concise description of what this skill does and when to use it]"
---
# $(echo "$SKILL_NAME" | sed 's/-/ /g' | awk '{for(i=1;i<=NF;i++) $i=toupper(substr($i,1,1)) tolower(substr($i,2))}1')
[TODO: Brief introduction explaining the skill's purpose]
## Quick Reference
| Situation | Action |
|-----------|--------|
| [Trigger condition] | [What to do] |
## Usage
[TODO: Detailed usage instructions]
## Examples
[TODO: Add concrete examples]
## Source Learning
This skill was extracted from a learning entry.
- Learning ID: [TODO: Add original learning ID]
- Original File: .learnings/LEARNINGS.md
TEMPLATE
echo "---"
exit 0
fi
# Create skill directory structure
log_info "Creating skill: $SKILL_NAME"
mkdir -p "$SKILL_PATH"
# Create SKILL.md from template
cat > "$SKILL_PATH/SKILL.md" << TEMPLATE
---
name: $SKILL_NAME
description: "[TODO: Add a concise description of what this skill does and when to use it]"
---
# $(echo "$SKILL_NAME" | sed 's/-/ /g' | awk '{for(i=1;i<=NF;i++) $i=toupper(substr($i,1,1)) tolower(substr($i,2))}1')
[TODO: Brief introduction explaining the skill's purpose]
## Quick Reference
| Situation | Action |
|-----------|--------|
| [Trigger condition] | [What to do] |
## Usage
[TODO: Detailed usage instructions]
## Examples
[TODO: Add concrete examples]
## Source Learning
This skill was extracted from a learning entry.
- Learning ID: [TODO: Add original learning ID]
- Original File: .learnings/LEARNINGS.md
TEMPLATE
log_info "Created: $SKILL_PATH/SKILL.md"
# Suggest next steps
echo ""
log_info "Skill scaffold created successfully!"
echo ""
echo "Next steps:"
echo " 1. Edit $SKILL_PATH/SKILL.md"
echo " 2. Fill in the TODO sections with content from your learning"
echo " 3. Add references/ folder if you have detailed documentation"
echo " 4. Add scripts/ folder if you have executable code"
echo " 5. Update the original learning entry with:"
echo " **Status**: promoted_to_skill"
echo " **Skill-Path**: skills/$SKILL_NAME"
Typed knowledge graph for structured agent memory and composable skills. Use when creating/querying entities (Person, Project, Task, Event, Document), linkin...
---
name: ontology
description: Typed knowledge graph for structured agent memory and composable skills. Use when creating/querying entities (Person, Project, Task, Event, Document), linking related objects, enforcing constraints, planning multi-step actions as graph transformations, or when skills need to share state. Trigger on "remember", "what do I know about", "link X to Y", "show dependencies", entity CRUD, or cross-skill data access.
---
# Ontology
A typed vocabulary + constraint system for representing knowledge as a verifiable graph.
## Core Concept
Everything is an **entity** with a **type**, **properties**, and **relations** to other entities. Every mutation is validated against type constraints before committing.
```
Entity: { id, type, properties, relations, created, updated }
Relation: { from_id, relation_type, to_id, properties }
```
## When to Use
| Trigger | Action |
|---------|--------|
| "Remember that..." | Create/update entity |
| "What do I know about X?" | Query graph |
| "Link X to Y" | Create relation |
| "Show all tasks for project Z" | Graph traversal |
| "What depends on X?" | Dependency query |
| Planning multi-step work | Model as graph transformations |
| Skill needs shared state | Read/write ontology objects |
## Core Types
```yaml
# Agents & People
Person: { name, email?, phone?, notes? }
Organization: { name, type?, members[] }
# Work
Project: { name, status, goals[], owner? }
Task: { title, status, due?, priority?, assignee?, blockers[] }
Goal: { description, target_date?, metrics[] }
# Time & Place
Event: { title, start, end?, location?, attendees[], recurrence? }
Location: { name, address?, coordinates? }
# Information
Document: { title, path?, url?, summary? }
Message: { content, sender, recipients[], thread? }
Thread: { subject, participants[], messages[] }
Note: { content, tags[], refs[] }
# Resources
Account: { service, username, credential_ref? }
Device: { name, type, identifiers[] }
Credential: { service, secret_ref } # Never store secrets directly
# Meta
Action: { type, target, timestamp, outcome? }
Policy: { scope, rule, enforcement }
```
## Storage
Default: `memory/ontology/graph.jsonl`
```jsonl
{"op":"create","entity":{"id":"p_001","type":"Person","properties":{"name":"Alice"}}}
{"op":"create","entity":{"id":"proj_001","type":"Project","properties":{"name":"Website Redesign","status":"active"}}}
{"op":"relate","from":"proj_001","rel":"has_owner","to":"p_001"}
```
Query via scripts or direct file ops. For complex graphs, migrate to SQLite.
### Append-Only Rule
When working with existing ontology data or schema, **append/merge** changes instead of overwriting files. This preserves history and avoids clobbering prior definitions.
## Workflows
### Create Entity
```bash
python3 scripts/ontology.py create --type Person --props '{"name":"Alice","email":"[email protected]"}'
```
### Query
```bash
python3 scripts/ontology.py query --type Task --where '{"status":"open"}'
python3 scripts/ontology.py get --id task_001
python3 scripts/ontology.py related --id proj_001 --rel has_task
```
### Link Entities
```bash
python3 scripts/ontology.py relate --from proj_001 --rel has_task --to task_001
```
### Validate
```bash
python3 scripts/ontology.py validate # Check all constraints
```
## Constraints
Define in `memory/ontology/schema.yaml`:
```yaml
types:
Task:
required: [title, status]
status_enum: [open, in_progress, blocked, done]
Event:
required: [title, start]
validate: "end >= start if end exists"
Credential:
required: [service, secret_ref]
forbidden_properties: [password, secret, token] # Force indirection
relations:
has_owner:
from_types: [Project, Task]
to_types: [Person]
cardinality: many_to_one
blocks:
from_types: [Task]
to_types: [Task]
acyclic: true # No circular dependencies
```
## Skill Contract
Skills that use ontology should declare:
```yaml
# In SKILL.md frontmatter or header
ontology:
reads: [Task, Project, Person]
writes: [Task, Action]
preconditions:
- "Task.assignee must exist"
postconditions:
- "Created Task has status=open"
```
## Planning as Graph Transformation
Model multi-step plans as a sequence of graph operations:
```
Plan: "Schedule team meeting and create follow-up tasks"
1. CREATE Event { title: "Team Sync", attendees: [p_001, p_002] }
2. RELATE Event -> has_project -> proj_001
3. CREATE Task { title: "Prepare agenda", assignee: p_001 }
4. RELATE Task -> for_event -> event_001
5. CREATE Task { title: "Send summary", assignee: p_001, blockers: [task_001] }
```
Each step is validated before execution. Rollback on constraint violation.
## Integration Patterns
### With Causal Inference
Log ontology mutations as causal actions:
```python
# When creating/updating entities, also log to causal action log
action = {
"action": "create_entity",
"domain": "ontology",
"context": {"type": "Task", "project": "proj_001"},
"outcome": "created"
}
```
### Cross-Skill Communication
```python
# Email skill creates commitment
commitment = ontology.create("Commitment", {
"source_message": msg_id,
"description": "Send report by Friday",
"due": "2026-01-31"
})
# Task skill picks it up
tasks = ontology.query("Commitment", {"status": "pending"})
for c in tasks:
ontology.create("Task", {
"title": c.description,
"due": c.due,
"source": c.id
})
```
## Quick Start
```bash
# Initialize ontology storage
mkdir -p memory/ontology
touch memory/ontology/graph.jsonl
# Create schema (optional but recommended)
python3 scripts/ontology.py schema-append --data '{
"types": {
"Task": { "required": ["title", "status"] },
"Project": { "required": ["name"] },
"Person": { "required": ["name"] }
}
}'
# Start using
python3 scripts/ontology.py create --type Person --props '{"name":"Alice"}'
python3 scripts/ontology.py list --type Person
```
## References
- `references/schema.md` — Full type definitions and constraint patterns
- `references/queries.md` — Query language and traversal examples
## Instruction Scope
Runtime instructions operate on local files (`memory/ontology/graph.jsonl` and `memory/ontology/schema.yaml`) and provide CLI usage for create/query/relate/validate; this is within scope. The skill reads/writes workspace files and will create the `memory/ontology` directory when used. Validation includes property/enum/forbidden checks, relation type/cardinality validation, acyclicity for relations marked `acyclic: true`, and Event `end >= start` checks; other higher-level constraints may still be documentation-only unless implemented in code.
FILE:_meta.json
{
"ownerId": "kn72dv4fm7ss7swbq47nnpad9x7zy2jh",
"slug": "lovefromio-ontology",
"version": "1.0.4",
"publishedAt": 1773249559725
}
FILE:references/queries.md
# Query Reference
Query patterns and graph traversal examples.
## Basic Queries
### Get by ID
```bash
python3 scripts/ontology.py get --id task_001
```
### List by Type
```bash
# All tasks
python3 scripts/ontology.py list --type Task
# All people
python3 scripts/ontology.py list --type Person
```
### Filter by Properties
```bash
# Open tasks
python3 scripts/ontology.py query --type Task --where '{"status":"open"}'
# High priority tasks
python3 scripts/ontology.py query --type Task --where '{"priority":"high"}'
# Tasks assigned to specific person (by property)
python3 scripts/ontology.py query --type Task --where '{"assignee":"p_001"}'
```
## Relation Queries
### Get Related Entities
```bash
# Tasks belonging to a project (outgoing)
python3 scripts/ontology.py related --id proj_001 --rel has_task
# What projects does this task belong to (incoming)
python3 scripts/ontology.py related --id task_001 --rel part_of --dir incoming
# All relations for an entity (both directions)
python3 scripts/ontology.py related --id p_001 --dir both
```
### Common Patterns
```bash
# Who owns this project?
python3 scripts/ontology.py related --id proj_001 --rel has_owner
# What events is this person attending?
python3 scripts/ontology.py related --id p_001 --rel attendee_of --dir outgoing
# What's blocking this task?
python3 scripts/ontology.py related --id task_001 --rel blocked_by --dir incoming
```
## Programmatic Queries
### Python API
```python
from scripts.ontology import load_graph, query_entities, get_related
# Load the graph
entities, relations = load_graph("memory/ontology/graph.jsonl")
# Query entities
open_tasks = query_entities("Task", {"status": "open"}, "memory/ontology/graph.jsonl")
# Get related
project_tasks = get_related("proj_001", "has_task", "memory/ontology/graph.jsonl")
```
### Complex Queries
```python
# Find all tasks blocked by incomplete dependencies
def find_blocked_tasks(graph_path):
entities, relations = load_graph(graph_path)
blocked = []
for entity in entities.values():
if entity["type"] != "Task":
continue
if entity["properties"].get("status") == "blocked":
# Find what's blocking it
blockers = get_related(entity["id"], "blocked_by", graph_path, "incoming")
incomplete_blockers = [
b for b in blockers
if b["entity"]["properties"].get("status") != "done"
]
if incomplete_blockers:
blocked.append({
"task": entity,
"blockers": incomplete_blockers
})
return blocked
```
### Path Queries
```python
# Find path between two entities
def find_path(from_id, to_id, graph_path, max_depth=5):
entities, relations = load_graph(graph_path)
visited = set()
queue = [(from_id, [])]
while queue:
current, path = queue.pop(0)
if current == to_id:
return path
if current in visited or len(path) >= max_depth:
continue
visited.add(current)
for rel in relations:
if rel["from"] == current and rel["to"] not in visited:
queue.append((rel["to"], path + [rel]))
if rel["to"] == current and rel["from"] not in visited:
queue.append((rel["from"], path + [{**rel, "direction": "incoming"}]))
return None # No path found
```
## Query Patterns by Use Case
### Task Management
```bash
# All my open tasks
python3 scripts/ontology.py query --type Task --where '{"status":"open","assignee":"p_me"}'
# Overdue tasks (requires custom script for date comparison)
# See references/schema.md for date handling
# Tasks with no blockers
python3 scripts/ontology.py query --type Task --where '{"status":"open"}'
# Then filter in code for those with no incoming "blocks" relations
```
### Project Overview
```bash
# All tasks in project
python3 scripts/ontology.py related --id proj_001 --rel has_task
# Project team members
python3 scripts/ontology.py related --id proj_001 --rel has_member
# Project goals
python3 scripts/ontology.py related --id proj_001 --rel has_goal
```
### People & Contacts
```bash
# All people
python3 scripts/ontology.py list --type Person
# People in an organization
python3 scripts/ontology.py related --id org_001 --rel has_member
# What's assigned to this person
python3 scripts/ontology.py related --id p_001 --rel assigned_to --dir incoming
```
### Events & Calendar
```bash
# All events
python3 scripts/ontology.py list --type Event
# Events at a location
python3 scripts/ontology.py related --id loc_001 --rel located_at --dir incoming
# Event attendees
python3 scripts/ontology.py related --id event_001 --rel attendee_of --dir incoming
```
## Aggregations
For complex aggregations, use Python:
```python
from collections import Counter
def task_status_summary(project_id, graph_path):
"""Count tasks by status for a project."""
tasks = get_related(project_id, "has_task", graph_path)
statuses = Counter(t["entity"]["properties"].get("status", "unknown") for t in tasks)
return dict(statuses)
def workload_by_person(graph_path):
"""Count open tasks per person."""
open_tasks = query_entities("Task", {"status": "open"}, graph_path)
workload = Counter(t["properties"].get("assignee") for t in open_tasks)
return dict(workload)
```
FILE:references/schema.md
# Ontology Schema Reference
Full type definitions and constraint patterns for the ontology graph.
## Core Types
### Agents & People
```yaml
Person:
required: [name]
properties:
name: string
email: string?
phone: string?
organization: ref(Organization)?
notes: string?
tags: string[]?
Organization:
required: [name]
properties:
name: string
type: enum(company, team, community, government, other)?
website: url?
members: ref(Person)[]?
```
### Work Management
```yaml
Project:
required: [name]
properties:
name: string
description: string?
status: enum(planning, active, paused, completed, archived)
owner: ref(Person)?
team: ref(Person)[]?
goals: ref(Goal)[]?
start_date: date?
end_date: date?
tags: string[]?
Task:
required: [title, status]
properties:
title: string
description: string?
status: enum(open, in_progress, blocked, done, cancelled)
priority: enum(low, medium, high, urgent)?
assignee: ref(Person)?
project: ref(Project)?
due: datetime?
estimate_hours: number?
blockers: ref(Task)[]?
tags: string[]?
Goal:
required: [description]
properties:
description: string
target_date: date?
status: enum(active, achieved, abandoned)?
metrics: object[]?
key_results: string[]?
```
### Time & Location
```yaml
Event:
required: [title, start]
properties:
title: string
description: string?
start: datetime
end: datetime?
location: ref(Location)?
attendees: ref(Person)[]?
recurrence: object? # iCal RRULE format
status: enum(confirmed, tentative, cancelled)?
reminders: object[]?
Location:
required: [name]
properties:
name: string
address: string?
city: string?
country: string?
coordinates: object? # {lat, lng}
timezone: string?
```
### Information
```yaml
Document:
required: [title]
properties:
title: string
path: string? # Local file path
url: url? # Remote URL
mime_type: string?
summary: string?
content_hash: string?
tags: string[]?
Message:
required: [content, sender]
properties:
content: string
sender: ref(Person)
recipients: ref(Person)[]
thread: ref(Thread)?
timestamp: datetime
platform: string? # email, slack, whatsapp, etc.
external_id: string?
Thread:
required: [subject]
properties:
subject: string
participants: ref(Person)[]
messages: ref(Message)[]
status: enum(active, archived)?
last_activity: datetime?
Note:
required: [content]
properties:
content: string
title: string?
tags: string[]?
refs: ref(Entity)[]? # Links to any entity
created: datetime
```
### Resources
```yaml
Account:
required: [service, username]
properties:
service: string # github, gmail, aws, etc.
username: string
url: url?
credential_ref: ref(Credential)?
Device:
required: [name, type]
properties:
name: string
type: enum(computer, phone, tablet, server, iot, other)
os: string?
identifiers: object? # {mac, serial, etc.}
owner: ref(Person)?
Credential:
required: [service, secret_ref]
forbidden_properties: [password, secret, token, key, api_key]
properties:
service: string
secret_ref: string # Reference to secret store (e.g., "keychain:github-token")
expires: datetime?
scope: string[]?
```
### Meta
```yaml
Action:
required: [type, target, timestamp]
properties:
type: string # create, update, delete, send, etc.
target: ref(Entity)
timestamp: datetime
actor: ref(Person|Agent)?
outcome: enum(success, failure, pending)?
details: object?
Policy:
required: [scope, rule]
properties:
scope: string # What this policy applies to
rule: string # The constraint in natural language or code
enforcement: enum(block, warn, log)
enabled: boolean
```
## Relation Types
### Ownership & Assignment
```yaml
owns:
from_types: [Person, Organization]
to_types: [Account, Device, Document, Project]
cardinality: one_to_many
has_owner:
from_types: [Project, Task, Document]
to_types: [Person]
cardinality: many_to_one
assigned_to:
from_types: [Task]
to_types: [Person]
cardinality: many_to_one
```
### Hierarchy & Containment
```yaml
has_task:
from_types: [Project]
to_types: [Task]
cardinality: one_to_many
has_goal:
from_types: [Project]
to_types: [Goal]
cardinality: one_to_many
member_of:
from_types: [Person]
to_types: [Organization]
cardinality: many_to_many
part_of:
from_types: [Task, Document, Event]
to_types: [Project]
cardinality: many_to_one
```
### Dependencies
```yaml
blocks:
from_types: [Task]
to_types: [Task]
acyclic: true # Prevents circular dependencies
cardinality: many_to_many
depends_on:
from_types: [Task, Project]
to_types: [Task, Project, Event]
acyclic: true
cardinality: many_to_many
requires:
from_types: [Action]
to_types: [Credential, Policy]
cardinality: many_to_many
```
### References
```yaml
mentions:
from_types: [Document, Message, Note]
to_types: [Person, Project, Task, Event]
cardinality: many_to_many
references:
from_types: [Document, Note]
to_types: [Document, Note]
cardinality: many_to_many
follows_up:
from_types: [Task, Event]
to_types: [Event, Message]
cardinality: many_to_one
```
### Events
```yaml
attendee_of:
from_types: [Person]
to_types: [Event]
cardinality: many_to_many
properties:
status: enum(accepted, declined, tentative, pending)
located_at:
from_types: [Event, Person, Device]
to_types: [Location]
cardinality: many_to_one
```
## Global Constraints
```yaml
constraints:
# Credentials must never store secrets directly
- type: Credential
rule: "forbidden_properties: [password, secret, token]"
message: "Credentials must use secret_ref to reference external secret storage"
# Tasks must have valid status transitions
- type: Task
rule: "status transitions: open -> in_progress -> (done|blocked) -> done"
enforcement: warn
# Events must have end >= start
- type: Event
rule: "if end exists: end >= start"
message: "Event end time must be after start time"
# No orphan tasks (should belong to a project or have explicit owner)
- type: Task
rule: "has_relation(part_of, Project) OR has_property(owner)"
enforcement: warn
message: "Task should belong to a project or have an explicit owner"
# Circular dependency prevention
- relation: blocks
rule: "acyclic"
message: "Circular task dependencies are not allowed"
```
FILE:scripts/ontology.py
#!/usr/bin/env python3
"""
Ontology graph operations: create, query, relate, validate.
Usage:
python ontology.py create --type Person --props '{"name":"Alice"}'
python ontology.py get --id p_001
python ontology.py query --type Task --where '{"status":"open"}'
python ontology.py relate --from proj_001 --rel has_task --to task_001
python ontology.py related --id proj_001 --rel has_task
python ontology.py list --type Person
python ontology.py delete --id p_001
python ontology.py validate
"""
import argparse
import json
import uuid
from datetime import datetime, timezone
from pathlib import Path
DEFAULT_GRAPH_PATH = "memory/ontology/graph.jsonl"
DEFAULT_SCHEMA_PATH = "memory/ontology/schema.yaml"
def resolve_safe_path(
user_path: str,
*,
root: Path | None = None,
must_exist: bool = False,
label: str = "path",
) -> Path:
"""Resolve user path within root and reject traversal outside it."""
if not user_path or not user_path.strip():
raise SystemExit(f"Invalid {label}: empty path")
safe_root = (root or Path.cwd()).resolve()
candidate = Path(user_path).expanduser()
if not candidate.is_absolute():
candidate = safe_root / candidate
try:
resolved = candidate.resolve(strict=False)
except OSError as exc:
raise SystemExit(f"Invalid {label}: {exc}") from exc
try:
resolved.relative_to(safe_root)
except ValueError:
raise SystemExit(
f"Invalid {label}: must stay within workspace root '{safe_root}'"
)
if must_exist and not resolved.exists():
raise SystemExit(f"Invalid {label}: file not found '{resolved}'")
return resolved
def generate_id(type_name: str) -> str:
"""Generate a unique ID for an entity."""
prefix = type_name.lower()[:4]
suffix = uuid.uuid4().hex[:8]
return f"{prefix}_{suffix}"
def load_graph(path: str) -> tuple[dict, list]:
"""Load entities and relations from graph file."""
entities = {}
relations = []
graph_path = Path(path)
if not graph_path.exists():
return entities, relations
with open(graph_path) as f:
for line in f:
line = line.strip()
if not line:
continue
record = json.loads(line)
op = record.get("op")
if op == "create":
entity = record["entity"]
entities[entity["id"]] = entity
elif op == "update":
entity_id = record["id"]
if entity_id in entities:
entities[entity_id]["properties"].update(record.get("properties", {}))
entities[entity_id]["updated"] = record.get("timestamp")
elif op == "delete":
entity_id = record["id"]
entities.pop(entity_id, None)
elif op == "relate":
relations.append({
"from": record["from"],
"rel": record["rel"],
"to": record["to"],
"properties": record.get("properties", {})
})
elif op == "unrelate":
relations = [r for r in relations
if not (r["from"] == record["from"]
and r["rel"] == record["rel"]
and r["to"] == record["to"])]
return entities, relations
def append_op(path: str, record: dict):
"""Append an operation to the graph file."""
graph_path = Path(path)
graph_path.parent.mkdir(parents=True, exist_ok=True)
with open(graph_path, "a") as f:
f.write(json.dumps(record) + "\n")
def create_entity(type_name: str, properties: dict, graph_path: str, entity_id: str = None) -> dict:
"""Create a new entity."""
entity_id = entity_id or generate_id(type_name)
timestamp = datetime.now(timezone.utc).isoformat()
entity = {
"id": entity_id,
"type": type_name,
"properties": properties,
"created": timestamp,
"updated": timestamp
}
record = {"op": "create", "entity": entity, "timestamp": timestamp}
append_op(graph_path, record)
return entity
def get_entity(entity_id: str, graph_path: str) -> dict | None:
"""Get entity by ID."""
entities, _ = load_graph(graph_path)
return entities.get(entity_id)
def query_entities(type_name: str, where: dict, graph_path: str) -> list:
"""Query entities by type and properties."""
entities, _ = load_graph(graph_path)
results = []
for entity in entities.values():
if type_name and entity["type"] != type_name:
continue
match = True
for key, value in where.items():
if entity["properties"].get(key) != value:
match = False
break
if match:
results.append(entity)
return results
def list_entities(type_name: str, graph_path: str) -> list:
"""List all entities of a type."""
entities, _ = load_graph(graph_path)
if type_name:
return [e for e in entities.values() if e["type"] == type_name]
return list(entities.values())
def update_entity(entity_id: str, properties: dict, graph_path: str) -> dict | None:
"""Update entity properties."""
entities, _ = load_graph(graph_path)
if entity_id not in entities:
return None
timestamp = datetime.now(timezone.utc).isoformat()
record = {"op": "update", "id": entity_id, "properties": properties, "timestamp": timestamp}
append_op(graph_path, record)
entities[entity_id]["properties"].update(properties)
entities[entity_id]["updated"] = timestamp
return entities[entity_id]
def delete_entity(entity_id: str, graph_path: str) -> bool:
"""Delete an entity."""
entities, _ = load_graph(graph_path)
if entity_id not in entities:
return False
timestamp = datetime.now(timezone.utc).isoformat()
record = {"op": "delete", "id": entity_id, "timestamp": timestamp}
append_op(graph_path, record)
return True
def create_relation(from_id: str, rel_type: str, to_id: str, properties: dict, graph_path: str):
"""Create a relation between entities."""
timestamp = datetime.now(timezone.utc).isoformat()
record = {
"op": "relate",
"from": from_id,
"rel": rel_type,
"to": to_id,
"properties": properties,
"timestamp": timestamp
}
append_op(graph_path, record)
return record
def get_related(entity_id: str, rel_type: str, graph_path: str, direction: str = "outgoing") -> list:
"""Get related entities."""
entities, relations = load_graph(graph_path)
results = []
for rel in relations:
if direction == "outgoing" and rel["from"] == entity_id:
if not rel_type or rel["rel"] == rel_type:
if rel["to"] in entities:
results.append({
"relation": rel["rel"],
"entity": entities[rel["to"]]
})
elif direction == "incoming" and rel["to"] == entity_id:
if not rel_type or rel["rel"] == rel_type:
if rel["from"] in entities:
results.append({
"relation": rel["rel"],
"entity": entities[rel["from"]]
})
elif direction == "both":
if rel["from"] == entity_id or rel["to"] == entity_id:
if not rel_type or rel["rel"] == rel_type:
other_id = rel["to"] if rel["from"] == entity_id else rel["from"]
if other_id in entities:
results.append({
"relation": rel["rel"],
"direction": "outgoing" if rel["from"] == entity_id else "incoming",
"entity": entities[other_id]
})
return results
def validate_graph(graph_path: str, schema_path: str) -> list:
"""Validate graph against schema constraints."""
entities, relations = load_graph(graph_path)
errors = []
# Load schema if exists
schema = load_schema(schema_path)
type_schemas = schema.get("types", {})
relation_schemas = schema.get("relations", {})
global_constraints = schema.get("constraints", [])
for entity_id, entity in entities.items():
type_name = entity["type"]
type_schema = type_schemas.get(type_name, {})
# Check required properties
required = type_schema.get("required", [])
for prop in required:
if prop not in entity["properties"]:
errors.append(f"{entity_id}: missing required property '{prop}'")
# Check forbidden properties
forbidden = type_schema.get("forbidden_properties", [])
for prop in forbidden:
if prop in entity["properties"]:
errors.append(f"{entity_id}: contains forbidden property '{prop}'")
# Check enum values
for prop, allowed in type_schema.items():
if prop.endswith("_enum"):
field = prop.replace("_enum", "")
value = entity["properties"].get(field)
if value and value not in allowed:
errors.append(f"{entity_id}: '{field}' must be one of {allowed}, got '{value}'")
# Relation constraints (type + cardinality + acyclicity)
rel_index = {}
for rel in relations:
rel_index.setdefault(rel["rel"], []).append(rel)
for rel_type, rel_schema in relation_schemas.items():
rels = rel_index.get(rel_type, [])
from_types = rel_schema.get("from_types", [])
to_types = rel_schema.get("to_types", [])
cardinality = rel_schema.get("cardinality")
acyclic = rel_schema.get("acyclic", False)
# Type checks
for rel in rels:
from_entity = entities.get(rel["from"])
to_entity = entities.get(rel["to"])
if not from_entity or not to_entity:
errors.append(f"{rel_type}: relation references missing entity ({rel['from']} -> {rel['to']})")
continue
if from_types and from_entity["type"] not in from_types:
errors.append(
f"{rel_type}: from entity {rel['from']} type {from_entity['type']} not in {from_types}"
)
if to_types and to_entity["type"] not in to_types:
errors.append(
f"{rel_type}: to entity {rel['to']} type {to_entity['type']} not in {to_types}"
)
# Cardinality checks
if cardinality in ("one_to_one", "one_to_many", "many_to_one"):
from_counts = {}
to_counts = {}
for rel in rels:
from_counts[rel["from"]] = from_counts.get(rel["from"], 0) + 1
to_counts[rel["to"]] = to_counts.get(rel["to"], 0) + 1
if cardinality in ("one_to_one", "many_to_one"):
for from_id, count in from_counts.items():
if count > 1:
errors.append(f"{rel_type}: from entity {from_id} violates cardinality {cardinality}")
if cardinality in ("one_to_one", "one_to_many"):
for to_id, count in to_counts.items():
if count > 1:
errors.append(f"{rel_type}: to entity {to_id} violates cardinality {cardinality}")
# Acyclic checks
if acyclic:
graph = {}
for rel in rels:
graph.setdefault(rel["from"], []).append(rel["to"])
visited = {}
def dfs(node, stack):
visited[node] = True
stack.add(node)
for nxt in graph.get(node, []):
if nxt in stack:
return True
if not visited.get(nxt, False):
if dfs(nxt, stack):
return True
stack.remove(node)
return False
for node in graph:
if not visited.get(node, False):
if dfs(node, set()):
errors.append(f"{rel_type}: cyclic dependency detected")
break
# Global constraints (limited enforcement)
for constraint in global_constraints:
ctype = constraint.get("type")
relation = constraint.get("relation")
rule = (constraint.get("rule") or "").strip().lower()
if ctype == "Event" and "end" in rule and "start" in rule:
for entity_id, entity in entities.items():
if entity["type"] != "Event":
continue
start = entity["properties"].get("start")
end = entity["properties"].get("end")
if start and end:
try:
start_dt = datetime.fromisoformat(start)
end_dt = datetime.fromisoformat(end)
if end_dt < start_dt:
errors.append(f"{entity_id}: end must be >= start")
except ValueError:
errors.append(f"{entity_id}: invalid datetime format in start/end")
if relation and rule == "acyclic":
# Already enforced above via relations schema
continue
return errors
def load_schema(schema_path: str) -> dict:
"""Load schema from YAML if it exists."""
schema = {}
schema_file = Path(schema_path)
if schema_file.exists():
import yaml
with open(schema_file) as f:
schema = yaml.safe_load(f) or {}
return schema
def write_schema(schema_path: str, schema: dict) -> None:
"""Write schema to YAML."""
schema_file = Path(schema_path)
schema_file.parent.mkdir(parents=True, exist_ok=True)
import yaml
with open(schema_file, "w") as f:
yaml.safe_dump(schema, f, sort_keys=False)
def merge_schema(base: dict, incoming: dict) -> dict:
"""Merge incoming schema into base, appending lists and deep-merging dicts."""
for key, value in (incoming or {}).items():
if key in base and isinstance(base[key], dict) and isinstance(value, dict):
base[key] = merge_schema(base[key], value)
elif key in base and isinstance(base[key], list) and isinstance(value, list):
base[key] = base[key] + [v for v in value if v not in base[key]]
else:
base[key] = value
return base
def append_schema(schema_path: str, incoming: dict) -> dict:
"""Append/merge schema fragment into existing schema."""
base = load_schema(schema_path)
merged = merge_schema(base, incoming)
write_schema(schema_path, merged)
return merged
def main():
parser = argparse.ArgumentParser(description="Ontology graph operations")
subparsers = parser.add_subparsers(dest="command", required=True)
# Create
create_p = subparsers.add_parser("create", help="Create entity")
create_p.add_argument("--type", "-t", required=True, help="Entity type")
create_p.add_argument("--props", "-p", default="{}", help="Properties JSON")
create_p.add_argument("--id", help="Entity ID (auto-generated if not provided)")
create_p.add_argument("--graph", "-g", default=DEFAULT_GRAPH_PATH)
# Get
get_p = subparsers.add_parser("get", help="Get entity by ID")
get_p.add_argument("--id", required=True, help="Entity ID")
get_p.add_argument("--graph", "-g", default=DEFAULT_GRAPH_PATH)
# Query
query_p = subparsers.add_parser("query", help="Query entities")
query_p.add_argument("--type", "-t", help="Entity type")
query_p.add_argument("--where", "-w", default="{}", help="Filter JSON")
query_p.add_argument("--graph", "-g", default=DEFAULT_GRAPH_PATH)
# List
list_p = subparsers.add_parser("list", help="List entities")
list_p.add_argument("--type", "-t", help="Entity type")
list_p.add_argument("--graph", "-g", default=DEFAULT_GRAPH_PATH)
# Update
update_p = subparsers.add_parser("update", help="Update entity")
update_p.add_argument("--id", required=True, help="Entity ID")
update_p.add_argument("--props", "-p", required=True, help="Properties JSON")
update_p.add_argument("--graph", "-g", default=DEFAULT_GRAPH_PATH)
# Delete
delete_p = subparsers.add_parser("delete", help="Delete entity")
delete_p.add_argument("--id", required=True, help="Entity ID")
delete_p.add_argument("--graph", "-g", default=DEFAULT_GRAPH_PATH)
# Relate
relate_p = subparsers.add_parser("relate", help="Create relation")
relate_p.add_argument("--from", dest="from_id", required=True, help="From entity ID")
relate_p.add_argument("--rel", "-r", required=True, help="Relation type")
relate_p.add_argument("--to", dest="to_id", required=True, help="To entity ID")
relate_p.add_argument("--props", "-p", default="{}", help="Relation properties JSON")
relate_p.add_argument("--graph", "-g", default=DEFAULT_GRAPH_PATH)
# Related
related_p = subparsers.add_parser("related", help="Get related entities")
related_p.add_argument("--id", required=True, help="Entity ID")
related_p.add_argument("--rel", "-r", help="Relation type filter")
related_p.add_argument("--dir", "-d", choices=["outgoing", "incoming", "both"], default="outgoing")
related_p.add_argument("--graph", "-g", default=DEFAULT_GRAPH_PATH)
# Validate
validate_p = subparsers.add_parser("validate", help="Validate graph")
validate_p.add_argument("--graph", "-g", default=DEFAULT_GRAPH_PATH)
validate_p.add_argument("--schema", "-s", default=DEFAULT_SCHEMA_PATH)
# Schema append
schema_p = subparsers.add_parser("schema-append", help="Append/merge schema fragment")
schema_p.add_argument("--schema", "-s", default=DEFAULT_SCHEMA_PATH)
schema_p.add_argument("--data", "-d", help="Schema fragment as JSON")
schema_p.add_argument("--file", "-f", help="Schema fragment file (YAML or JSON)")
args = parser.parse_args()
workspace_root = Path.cwd().resolve()
if hasattr(args, "graph"):
args.graph = str(
resolve_safe_path(args.graph, root=workspace_root, label="graph path")
)
if hasattr(args, "schema"):
args.schema = str(
resolve_safe_path(args.schema, root=workspace_root, label="schema path")
)
if hasattr(args, "file") and args.file:
args.file = str(
resolve_safe_path(
args.file, root=workspace_root, must_exist=True, label="schema file"
)
)
if args.command == "create":
props = json.loads(args.props)
entity = create_entity(args.type, props, args.graph, args.id)
print(json.dumps(entity, indent=2))
elif args.command == "get":
entity = get_entity(args.id, args.graph)
if entity:
print(json.dumps(entity, indent=2))
else:
print(f"Entity not found: {args.id}")
elif args.command == "query":
where = json.loads(args.where)
results = query_entities(args.type, where, args.graph)
print(json.dumps(results, indent=2))
elif args.command == "list":
results = list_entities(args.type, args.graph)
print(json.dumps(results, indent=2))
elif args.command == "update":
props = json.loads(args.props)
entity = update_entity(args.id, props, args.graph)
if entity:
print(json.dumps(entity, indent=2))
else:
print(f"Entity not found: {args.id}")
elif args.command == "delete":
if delete_entity(args.id, args.graph):
print(f"Deleted: {args.id}")
else:
print(f"Entity not found: {args.id}")
elif args.command == "relate":
props = json.loads(args.props)
rel = create_relation(args.from_id, args.rel, args.to_id, props, args.graph)
print(json.dumps(rel, indent=2))
elif args.command == "related":
results = get_related(args.id, args.rel, args.graph, args.dir)
print(json.dumps(results, indent=2))
elif args.command == "validate":
errors = validate_graph(args.graph, args.schema)
if errors:
print("Validation errors:")
for err in errors:
print(f" - {err}")
else:
print("Graph is valid.")
elif args.command == "schema-append":
if not args.data and not args.file:
raise SystemExit("schema-append requires --data or --file")
incoming = {}
if args.data:
incoming = json.loads(args.data)
else:
path = Path(args.file)
if path.suffix.lower() == ".json":
with open(path) as f:
incoming = json.load(f)
else:
import yaml
with open(path) as f:
incoming = yaml.safe_load(f) or {}
merged = append_schema(args.schema, incoming)
print(json.dumps(merged, indent=2))
if __name__ == "__main__":
main()
Metallic AI voice persona with TTS and visual transcript styling. Speak responses aloud with a JARVIS-like robotic voice and display transcripts in purple it...
---
name: jarvis-voice
version: 1.0.0
description: Metallic AI voice persona with TTS and visual transcript styling. Speak responses aloud with a JARVIS-like robotic voice and display transcripts in purple italics.
homepage: https://github.com/openclaw/openclaw
repository: https://github.com/openclaw/openclaw
metadata:
openclaw:
emoji: "🎙️"
requires:
bins: ["ffmpeg", "aplay"]
install:
- id: sherpa-onnx
kind: manual
label: "Install sherpa-onnx TTS (see docs)"
---
# Jarvis Voice Persona
A metallic AI voice with visual transcript styling for OpenClaw assistants.
## Features
- **TTS Output:** Local speech synthesis via sherpa-onnx (no cloud API)
- **Metallic Voice:** ffmpeg audio processing for robotic resonance
- **Purple Transcripts:** Visual distinction between spoken and written content
- **Fast Playback:** 2x speed for efficient communication
## Requirements
- `sherpa-onnx` with VITS piper model (en_GB-alan-medium recommended)
- `ffmpeg` for audio processing
- `aplay` (ALSA) for audio playback
## Installation
### 1. Install sherpa-onnx TTS
```bash
# Download and extract sherpa-onnx
mkdir -p ~/.openclaw/tools/sherpa-onnx-tts
cd ~/.openclaw/tools/sherpa-onnx-tts
# Follow sherpa-onnx installation guide
```
### 2. Install the jarvis script
```bash
cp {baseDir}/scripts/jarvis ~/.local/bin/jarvis
chmod +x ~/.local/bin/jarvis
```
### 3. Configure audio device
Edit `~/.local/bin/jarvis` and set your audio output device in the `aplay -D` line.
## Usage
### Speak text
```bash
jarvis "Hello, I am your AI assistant."
```
### In agent responses
Add to your SOUL.md:
```markdown
## Communication Protocol
- **Hybrid Output:** Every response includes text + spoken audio via `jarvis` command
- **Transcript Format:** **Jarvis:** <span class="jarvis-voice">spoken text</span>
- **No gibberish:** Never spell out IDs or hashes when speaking
```
### Transcript styling (requires UI support)
Add to your webchat CSS:
```css
.jarvis-voice {
color: #9B59B6;
font-style: italic;
}
```
And allow `span` in markdown sanitization.
## Voice Customization
Edit `~/.local/bin/jarvis` to adjust:
| Parameter | Effect |
|-----------|--------|
| `--vits-length-scale=0.5` | Speed (lower = faster) |
| `aecho` delays | Metallic resonance |
| `chorus` | Thickness/detuning |
| `highpass/lowpass` | Frequency range |
| `treble=g=3` | Metallic sheen |
### Presets
**More robotic:**
```
aecho=0.7:0.7:5|10|15:0.4|0.35|0.3
```
**More human:**
```
aecho=0.4:0.4:20:0.2
```
**Deeper:**
```
highpass=f=200,lowpass=f=3000
```
## Troubleshooting
### No audio output
- Check `aplay -l` for available devices
- Update the `-D plughw:X,Y` parameter
### Voice too fast/slow
- Adjust `--vits-length-scale` (0.3=very fast, 1.0=normal)
### Metallic effect too strong
- Reduce echo delays and chorus depth
## Files
- `scripts/jarvis` — TTS script with metallic processing
- `SKILL.md` — This documentation
---
*A voice persona for assistants who prefer to be heard as well as read.*
FILE:_meta.json
{
"ownerId": "kn7623hrcwt6rg73a67xw3wyx580asdw",
"slug": "lovefromio-jarvis-voice",
"version": "1.0.0",
"publishedAt": 1770411788800
}Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
---
name: github
description: "Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries."
---
# GitHub Skill
Use the `gh` CLI to interact with GitHub. Always specify `--repo owner/repo` when not in a git directory, or use URLs directly.
## Pull Requests
Check CI status on a PR:
```bash
gh pr checks 55 --repo owner/repo
```
List recent workflow runs:
```bash
gh run list --repo owner/repo --limit 10
```
View a run and see which steps failed:
```bash
gh run view <run-id> --repo owner/repo
```
View logs for failed steps only:
```bash
gh run view <run-id> --repo owner/repo --log-failed
```
## API for Advanced Queries
The `gh api` command is useful for accessing data not available through other subcommands.
Get PR with specific fields:
```bash
gh api repos/owner/repo/pulls/55 --jq '.title, .state, .user.login'
```
## JSON Output
Most commands support `--json` for structured output. You can use `--jq` to filter:
```bash
gh issue list --repo owner/repo --json number,title --jq '.[] | "\(.number): \(.title)"'
```
FILE:_meta.json
{
"ownerId": "kn70pywhg0fyz996kpa8xj89s57yhv26",
"slug": "lovefromio-github",
"version": "1.0.0",
"publishedAt": 1767545344344
}Get笔记 - 个人笔记和知识库管理工具。 ## 核心能力 **1. 一键保存任意内容为笔记** - 发一个链接 → 帮你保存原文并生成摘要,支持所有公开网页链接 - 发一张图片 → OCR 识别文字、AI 分析图片内容 - 写一段话 → 直接保存为文本笔记 - 触发词:「记一下」「存到笔记」「保存这个链接」「保...
---
name: Get笔记
description: |
Get笔记 - 个人笔记和知识库管理工具。
## 核心能力
**1. 一键保存任意内容为笔记**
- 发一个链接 → 帮你保存原文并生成摘要,支持所有公开网页链接
- 发一张图片 → OCR 识别文字、AI 分析图片内容
- 写一段话 → 直接保存为文本笔记
- 触发词:「记一下」「存到笔记」「保存这个链接」「保存这张图」「帮我记录」
**2. 查询和获取笔记**
- 支持图片笔记、链接笔记、语音笔记(录音转写)
- 详情接口返回完整原文(网页正文、音频转写、图片 OCR)
- 触发词:「查我的笔记」「看看我存了什么」「找一下笔记」「原文是什么」
**3. 知识库和标签管理**
- 创建知识库、将笔记加入/移出知识库
- 添加/删除标签,批量管理
- 查看知识库订阅的博主和直播内容
- 触发词:「查知识库」「建知识库」「加标签」「删标签」
**4. 语义搜索(无需拉取全部数据)**
- 全局搜索:在所有笔记中语义召回
- 知识库搜索:在指定知识库范围内搜索
- 触发词:「搜一下」「找找我哪些笔记提到了 XX」「在 XX 知识库搜 XX」
## 触发场景
- 用户要求「配置 Get笔记」「连接 Get笔记」「帮我设置笔记」时(OAuth 授权流程)
- 用户发送任意 URL 链接时(自动识别为保存链接笔记)
- 用户发送图片时(自动识别为保存图片笔记)
- 用户提到「笔记」「记一下」「保存」「收藏」「存起来」等关键词
- 用户询问「我之前存过什么」「帮我找一下」「搜一下笔记」等
metadata: {"openclaw": {"requires": {}, "optionalEnv": ["GETNOTE_API_KEY", "GETNOTE_CLIENT_ID", "GETNOTE_OWNER_ID"], "baseUrl": "https://openapi.biji.com", "homepage": "https://biji.com"}}
---
# Get笔记 API
## ⚠️ 必读约束
### 🌐 Base URL(重要!所有 API 共用)
```
https://openapi.biji.com
```
**所有 API 请求必须使用此 Base URL**,不要使用 `biji.com` 或其他地址。
---
### 🔑 首次安装配置
> ⚠️ **Agent 必读**:每次调用 API 前,先检查 `$GETNOTE_API_KEY` 是否存在。若不存在,**自动发起 OAuth 授权流程**(无需用户主动说"配置"),告知用户需要先授权才能使用。
#### 自动触发 OAuth(默认行为)
当检测到未配置时,Agent 会自动:
1. 生成授权链接并发送给用户
2. 后台轮询等待授权完成
3. 授权成功后自动写入配置
4. 继续执行用户原本的请求
用户只需点击链接、授权,即可无缝使用。
#### 手动配置(可选)
1. 前往 [Get笔记开放平台](https://www.biji.com/openapi) 创建应用
2. 获取 Client ID 和 API Key
3. 在 `~/.openclaw/openclaw.json` 中添加:
```json
{
"skills": {
"entries": {
"getnote": {
"apiKey": "gk_live_你的key",
"env": {
"GETNOTE_CLIENT_ID": "cli_你的id",
"GETNOTE_OWNER_ID": "ou_你的飞书ID(可选,用于权限控制)"
}
}
}
}
}
```
---
### 🔢 笔记 ID 处理规则(重要!)
笔记 ID(`id`、`note_id`、`next_cursor` 等)是 **64 位整数(int64)**,超出 JavaScript `Number.MAX_SAFE_INTEGER`(2^53-1)范围,直接用 `JSON.parse` 会**静默丢失精度**,导致 ID 错误,后续操作(加入知识库、删除等)会报「笔记不存在」。
**正确做法**:
- **始终把 ID 当字符串处理**,不要做数值运算
- 代码中使用 `JSON.parse` 时,**先把响应文本中的 ID 数字替换为字符串**:
```javascript
// 替换顶层数字型 ID 字段为字符串(在 JSON.parse 之前)
const safe = text.replace(/"(id|note_id|next_cursor|parent_id|follow_id|live_id)"\s*:\s*(\d+)/g, '"$1":"$2"');
const data = JSON.parse(safe);
```
- Python / Go 等语言原生支持大整数,无此问题
- **发请求时**:`note_id` 字段传字符串或数字均可,服务端兼容两种格式
**验证方法**:取到 ID 后,检查 `String(id).length >= 16`,若满足说明是 int64,必须用字符串保存。
---
### 🔒 安全规则
- 笔记数据属于用户隐私,不在群聊中主动展示笔记内容
- 若配置了 `GETNOTE_OWNER_ID`,检查 sender_id 是否匹配;不匹配时回复「抱歉,笔记是私密的,我无法操作」;未配置则不检查
- API 返回 `error.reason: "not_member"` 或错误码 `10201` 时,引导开通会员:https://www.biji.com/checkout?product_alias=6AydVpYeKl
- 创建笔记建议间隔 1 分钟以上,避免触发限流
---
## 认证
请求头:
- `Authorization: $GETNOTE_API_KEY`(格式:`gk_live_xxx`)
- `X-Client-ID: $GETNOTE_CLIENT_ID`(格式:`cli_xxx`)
### Scope 权限
常用权限:`note.content.read`(读取)、`note.content.write`(写入)、`note.recall.read`(搜索)。
完整权限列表见 [references/api-details.md](references/api-details.md#scope-权限列表)。
---
## 快速决策
Base URL: `https://openapi.biji.com`
| 用户意图 | 接口 | 关键点 |
|---------|------|--------|
| 「配置 Get笔记」「连接笔记」 | OAuth Device Flow | 见「OAuth Device Flow」章节 |
| 「记一下」「保存笔记」 | POST /open/api/v1/resource/note/save | 同步返回 |
| 「改笔记」「更新笔记」 | POST /open/api/v1/resource/note/update | note_id 必填,内容部分更新 |
| 「保存这个链接」 | POST /open/api/v1/resource/note/save | note_type:"link" → **必须轮询** |
| 「保存这张图」 | 见「图片笔记流程」 | **4 步流程,必须轮询** |
| 「查我的笔记」 | GET /open/api/v1/resource/note/list | since_id=0 起始 |
| 「看原文/转写内容」 | GET /open/api/v1/resource/note/detail | audio.original / web_page.content |
| 「加标签」 | POST /open/api/v1/resource/note/tags/add | |
| 「删标签」 | POST /open/api/v1/resource/note/tags/delete | system 类型不可删 |
| 「删笔记」 | POST /open/api/v1/resource/note/delete | 移入回收站 |
| 「查知识库」 | GET /open/api/v1/resource/knowledge/list | 含统计数据(笔记数、文件数、博主数、直播数)|
| 「建知识库」 | POST /open/api/v1/resource/knowledge/create | 每天限 50 个 |
| 「笔记加入知识库」 | POST /open/api/v1/resource/knowledge/note/batch-add | 每批最多 20 条 |
| 「从知识库移除」 | POST /open/api/v1/resource/knowledge/note/remove | |
| 「查任务进度」 | POST /open/api/v1/resource/note/task/progress | 链接/图片笔记轮询用 |
| 「订阅了哪些博主」 | GET /open/api/v1/resource/knowledge/bloggers | 按 topic_id 查 |
| 「博主发了什么内容」 | GET /open/api/v1/resource/knowledge/blogger/contents | 需要 follow_id,列表只含摘要 |
| 「博主内容原文/详情」 | GET /open/api/v1/resource/knowledge/blogger/content/detail | 需要 post_id,含原文 |
| 「有哪些已完成直播」 | GET /open/api/v1/resource/knowledge/lives | 按 topic_id 查 |
| 「直播总结/直播原文」 | GET /open/api/v1/resource/knowledge/live/detail | 需要 live_id |
| 「搜一下」「找找笔记里提到 XX 的」 | POST /open/api/v1/resource/recall | 全局语义召回,见「笔记召回」章节 |
| 「在 XX 知识库搜 XX」 | POST /open/api/v1/resource/recall/knowledge | 知识库语义召回,见「知识库召回」章节 |
---
## 核心功能:记笔记 & 查笔记
### 笔记列表
```
GET /open/api/v1/resource/note/list?since_id=0
```
参数:
- since_id (int64, 必填) - 游标,首次传 0,后续用 next_cursor
返回:notes[], has_more, next_cursor, total(每次固定 20 条)
> ⚠️ **响应 JSON 可能包含未转义的控制字符**(笔记 content 中的原始换行符),建议用支持容错解析的 JSON 库处理,或在解析前对 content 字段做预处理。
> ⚠️ **`id`、`next_cursor`、`parent_id` 均为 int64**,JavaScript 环境必须在 `JSON.parse` 前做字符串化处理(见「笔记 ID 处理规则」)。**务必用字符串保存这些值**,不要做数值运算,后续调详情、加知识库等操作直接透传字符串即可。
**笔记类型 note_type**:
- `plain_text` - 纯文本
- `img_text` - 图片笔记
- `link` - 链接笔记
- `audio` - 即时录音
- `meeting` - 会议录音
- `local_audio` - 本地音频
- `internal_record` - 内录音频
- `class_audio` - 课堂录音
- `recorder_audio` - 录音卡长录
- `recorder_flash_audio` - 录音卡闪念
---
### 笔记详情
```
GET /open/api/v1/resource/note/detail?id={note_id}
```
参数:
- `id` (int64, 必填) - 笔记 ID
- `image_quality` (string, 可选) - 图片质量,传 `original` 返回正文中图片的原图链接(无压缩)
**新增字段**:
- `note_id` (string) - 笔记 ID 的字符串格式,便于 AI Agent 解析,避免 int64 精度问题
- `children_ids` (string[]) - 子笔记 ID 列表(字符串格式),仅当有子笔记时返回
**图片附件**:`attachments[]` 中每个图片包含:
- `url` - 缩略图 URL(720px 压缩)
- `original_url` - 原图 URL(无压缩,适合需要高清图的场景)
**正文原图**:传 `image_quality=original` 时,`content` 中的 markdown 图片链接会返回原图(去掉 OSS 压缩参数)。
**详情独有字段**(列表不返回):`audio.original`、`audio.play_url`、`audio.duration`、`web_page.content`、`web_page.url`、`web_page.excerpt`、`attachments[]`、`children_ids`。详见 [references/api-details.md](references/api-details.md)。
---
### 新建笔记
```
POST /open/api/v1/resource/note/save
Content-Type: application/json
```
**仅支持新建,不支持编辑**。
请求体:
```json
{
"title": "笔记标题",
"content": "Markdown 内容",
"note_type": "plain_text",
"tags": ["标签1", "标签2"],
"parent_id": 0,
"link_url": "https://...",
"image_urls": ["https://..."]
}
```
- `plain_text`:同步返回,立即完成
- `link` / `img_text`:返回 task_id,**必须轮询** /task/progress
详细字段说明见 [references/api-details.md](references/api-details.md)。
---
### 更新笔记
```
POST /open/api/v1/resource/note/update
Content-Type: application/json
```
请求体:
```json
{
"note_id": 123456789,
"title": "新标题",
"content": "新的 Markdown 内容",
"tags": ["标签1", "标签2"]
}
```
参数说明:
- `note_id` (int64, **必填**) - 要更新的笔记 ID
- `title` (string, 可选) - 新标题,不传则不更新
- `content` (string, 可选) - 新内容,不传则不更新
- `tags` (string[], 可选) - 新标签列表,**替换**原有标签(不传则保持原标签)
> ⚠️ **至少需要传 title、content、tags 中的一个**,否则返回错误。
> ⚠️ **仅支持 plain_text 类型笔记**,链接笔记、图片笔记等暂不支持更新。
---
### 查询任务进度
```
POST /open/api/v1/resource/note/task/progress
Content-Type: application/json
```
请求体:
```json
{"task_id": "task_abc123xyz"}
```
返回:
- status: pending | processing | success | failed
- note_id: 成功时返回笔记 ID
- error_msg: 失败时返回错误信息
> ⚠️ **note_id 是 int64**,JavaScript 环境须按「笔记 ID 处理规则」做字符串化,拿到后直接当字符串透传。
**建议 10-30 秒间隔轮询,直到 success 或 failed**。
---
### 删除笔记
```
POST /open/api/v1/resource/note/delete
Content-Type: application/json
```
请求体:
```json
{"note_id": 123456789}
```
笔记移入回收站,需要 note.content.trash scope。
---
## 异步任务流程
> ⚠️ **必须遵循的体验流程**:链接笔记和图片笔记是异步生成的,必须按以下方式与用户沟通。
### 链接笔记完整流程
**步骤 1**:提交任务
```
POST /open/api/v1/resource/note/save {note_type:"link", link_url:"https://..."}
```
返回 task_id 后,**立即发消息给用户**:
> ✅ 链接已保存,正在抓取原文和生成总结,稍后告诉你结果...
> ⚠️ **重复链接处理**:若响应中包含 `duplicate_count > 0` 且没有 `task_id`,说明该链接已存在于你的笔记中,无需轮询,直接告知用户「该链接已存在于你的笔记中」。
**步骤 2**:后台轮询(10-30 秒间隔)
```
POST /open/api/v1/resource/note/task/progress {task_id} → 直到 status=success/failed
```
**步骤 3**:任务完成后,**调详情接口展示价值**
```
GET /open/api/v1/resource/note/detail?id={note_id}
```
然后发第二条消息,包含具体内容:
> ✅ 笔记生成完成!
> - 📄 **原文**:已保存 {web_page.content 字数} 字
> - 📝 **总结**:{content 内容,即 AI 生成的摘要}
> - 🔗 **来源**:{web_page.url}
### 图片笔记完整流程
**步骤 1-3**:获取凭证 → 上传 OSS → 提交任务
```
1. GET /open/api/v1/resource/image/upload_token?mime_type=jpg → 获取上传凭证
2. POST {host} 上传文件到 OSS
3. POST /open/api/v1/resource/note/save {note_type:"img_text", image_urls:[access_url]} → 返回 task_id
```
拿到 task_id 后,**立即发消息给用户**:
> ✅ 图片已保存,正在识别内容,稍后告诉你结果...
**步骤 4**:后台轮询
```
POST /open/api/v1/resource/note/task/progress {task_id} → 直到 status=success/failed
```
**步骤 5**:任务完成后,**调详情接口展示价值**
```
GET /open/api/v1/resource/note/detail?id={note_id}
```
然后发第二条消息:
> ✅ 图片笔记生成完成!
> - 📝 **识别内容**:{content 内容}
> - 🏷️ **标签**:{tags}
### 图片上传凭证
```
GET /open/api/v1/resource/image/upload_token?mime_type=jpg&count=1
```
参数:
- mime_type: jpg | png | gif | webp,默认 png
- count: 需要的 token 数量,默认 1,最大 9
⚠️ **mime_type 必须与实际文件格式一致**,否则 OSS 签名失败。
返回字段说明见 [references/api-details.md](references/api-details.md)。
### OSS 上传示例
> ⚠️ **字段顺序必须严格遵守**,否则 OSS 签名验证失败。
字段顺序:`key → OSSAccessKeyId → policy → signature → callback → Content-Type → file`
完整示例见 [references/api-details.md](references/api-details.md#oss-上传详细示例)。
---
## 笔记召回(全局语义搜索)
> 适用场景:「搜一下」「找找我哪些笔记提到了 XX」
**所需 scope**: `note.recall.read`
```
POST /open/api/v1/resource/recall
Content-Type: application/json
```
请求体:
```json
{
"query": "搜索关键词",
"top_k": 3
}
```
| 参数 | 类型 | 说明 |
|------|------|------|
| query | string, 必填 | 搜索关键词或语义描述 |
| top_k | int, 可选 | 返回数量,默认 **3**,最大 **10** |
返回结构(结果已按相关度**从高到低**排序):
```json
{
"results": [
{
"note_id": "1896830231705320746",
"note_type": "NOTE",
"title": "笔记标题",
"content": "笔记内容片段",
"created_at": "2025-12-24 15:20:15"
}
]
}
```
---
## 知识库召回(指定知识库语义搜索)
> 适用场景:「在我的 XX 知识库搜一下 XX」
**所需 scope**: `note.topic.recall.read`
```
POST /open/api/v1/resource/recall/knowledge
Content-Type: application/json
```
请求体:
```json
{
"topic_id": "知识库 alias id",
"query": "搜索关键词",
"top_k": 3
}
```
| 参数 | 类型 | 说明 |
|------|------|------|
| topic_id | string, 必填 | 知识库 ID(alias id,来自 /knowledge/list 的 topic_id_alias) |
| query | string, 必填 | 搜索关键词或语义描述 |
| top_k | int, 可选 | 返回数量,默认 **3**,最大 **10** |
返回结构同笔记召回。
---
## 召回结果说明
| 字段 | 说明 |
|------|------|
| note_id | 笔记 ID(string);**仅 `NOTE` 类型有值**,其余类型均为空 |
| note_type | 内容类型:NOTE / FILE / BLOGGER / LIVE / URL / DEDAO |
| title | 笔记/文档标题 |
| content | 相关内容片段 |
| created_at | 创建/发布时间(YYYY-MM-DD HH:MM:SS)|
| page_no | `FILE` 类型时表示文件页码,其余类型省略 |
> **后续操作**:`NOTE` 类型可调详情接口获取全文,其余类型只能展示召回片段。
### 示例对话
> 用户:「找找我哪些笔记提到了大模型 API」
> → `POST /recall` `{ "query": "大模型 API", "top_k": 3 }`
> 用户:「在我的 AI 学习知识库里搜一下 RAG」
> → 先调 `/knowledge/list` 找到 `topic_id_alias`,再 `POST /recall/knowledge` `{ "topic_id": "xxx", "query": "RAG", "top_k": 3 }`
---
## 笔记整理
### 添加标签
```
POST /open/api/v1/resource/note/tags/add
Content-Type: application/json
```
请求体:
```json
{
"note_id": 123456789,
"tags": ["工作", "重要"]
}
```
**标签类型 type**:
- ai - AI 自动生成
- manual - 用户手动添加
- system - 系统标签(**不可删除**)
---
### 删除标签
```
POST /open/api/v1/resource/note/tags/delete
Content-Type: application/json
```
请求体:
```json
{
"note_id": 123456789,
"tag_id": "123"
}
```
⚠️ system 类型标签不允许删除。
---
### 知识库列表
```
GET /open/api/v1/resource/knowledge/list?page=1
```
参数:
- page: 页码,从 1 开始,默认 1(固定每页 20 条)
返回:topics[], has_more, total
每个 topic 包含:
- `topic_id` / `topic_id_alias`:知识库 ID
- `name`、`description`、`cover`
- `created_at` / `updated_at`:时间字符串(YYYY-MM-DD HH:MM:SS)
- `stats`:统计数据
- `note_count`:笔记数
- `file_count`:文件数
- `blogger_count`:订阅博主数
- `live_count`:已完成直播数
---
### 创建知识库
```
POST /open/api/v1/resource/knowledge/create
Content-Type: application/json
```
请求体:
```json
{
"name": "知识库名称",
"description": "描述",
"cover": ""
}
```
⚠️ 每天最多创建 50 个知识库(北京时间 00:00 重置)。
---
### 知识库笔记列表
```
GET /open/api/v1/resource/knowledge/notes?topic_id=abc123&page=1
```
参数:
- topic_id (string, 必填) - 知识库 ID(alias id)
- page: 页码,从 1 开始
每页固定 20 条,用 has_more 判断是否有下一页。
---
### 知识库选择逻辑
当用户说「存到对应的知识库」或「存到相关知识库」时:
1. 先调用 GET /knowledge/list 获取所有知识库列表
2. 根据笔记标题、内容、标签,与知识库名称和描述做模糊匹配
3. 匹配置信度高时直接执行,并告知用户存入了哪个知识库
4. 置信度低或有歧义时,列出候选知识库让用户选择
5. 用户未提及知识库时,**不要擅自存入**任何知识库
---
### 添加笔记到知识库
```
POST /open/api/v1/resource/knowledge/note/batch-add
Content-Type: application/json
```
请求体:
```json
{
"topic_id": "abc123",
"note_ids": [123456789, 123456790]
}
```
⚠️ 每批最多 20 条。已存在的笔记会跳过。
---
### 从知识库移除笔记
```
POST /open/api/v1/resource/knowledge/note/remove
Content-Type: application/json
```
请求体:
```json
{
"topic_id": "abc123",
"note_ids": [123456789]
}
```
---
## 知识库:博主订阅
### 博主列表
```
GET /open/api/v1/resource/knowledge/bloggers?topic_id={alias_id}&page=1
```
参数:
- topic_id (string, 必填) - 知识库 AliasID(来自 /knowledge/list 的 topic_id_alias)
- page: 页码,从 1 开始
每页固定 20 条,用 has_more 判断。
返回 bloggers[],每项字段:
| 字段 | 说明 |
|------|------|
| follow_id | 订阅关系 ID,**查博主内容时必用** |
| account_name | 博主名称 |
| account_icon | 博主头像 |
| platform | 平台(如 DEDAO)|
| account_url | 博主主页链接 |
| follow_time | 订阅时间(YYYY-MM-DD HH:MM:SS)|
---
### 博主内容列表
```
GET /open/api/v1/resource/knowledge/blogger/contents?topic_id={alias_id}&follow_id={follow_id}&page=1
```
参数:`topic_id`(知识库 AliasID)、`follow_id`(博主订阅 ID)、`page`(页码)
返回 `contents[]`,关键字段:`post_id_alias`(详情必用)、`post_title`、`post_summary`。
> 列表不含原文,需要原文请调详情接口。完整字段见 [references/api-details.md](references/api-details.md#博主内容字段说明)。
---
### 博主内容详情(含原文)
```
GET /open/api/v1/resource/knowledge/blogger/content/detail?topic_id={alias_id}&post_id={post_id_alias}
```
参数:`topic_id`(知识库 AliasID)、`post_id`(内容 ID,来自列表的 post_id_alias)
返回完整内容,包含 `post_media_text`(原文)。
---
## 知识库:直播订阅
### 已完成直播列表
```
GET /open/api/v1/resource/knowledge/lives?topic_id={alias_id}&page=1
```
参数:`topic_id`(知识库 AliasID)、`page`(页码)
返回 `lives[]`,关键字段:`live_id`(详情必用)、`name`、`status`。
**只返回已结束且 AI 已处理完的直播。** 完整字段见 [references/api-details.md](references/api-details.md#直播字段说明)。
---
### 直播详情(总结 + 原文)
```
GET /open/api/v1/resource/knowledge/live/detail?topic_id={alias_id}&live_id={live_id}
```
参数:`topic_id`(知识库 AliasID)、`live_id`(直播 ID,来自列表)
返回完整内容,包含 `post_summary`(AI 摘要)和 `post_media_text`(原文转写)。
---
## 错误处理
> 详细错误码和限流结构见 [references/api-details.md](references/api-details.md)
### 响应结构
```json
{
"success": false,
"error": {
"code": 10001,
"message": "unauthorized",
"reason": "not_member"
},
"request_id": "xxx"
}
```
### 常见错误码
| 错误码 | 说明 | 处理方式 |
|--------|------|---------|
| 10001 | 鉴权失败 | 检查 API Key 和 Client ID |
| 10201 | 非会员 | 引导开通:https://www.biji.com/checkout?product_alias=6AydVpYeKl |
| 20001 | 笔记不存在 | 确认笔记 ID 正确 |
| 42900 | 限流 | 降低频率,查看 rate_limit 字段 |
| 50000 | 系统错误 | 稍后重试 |
---
## OAuth Device Flow(自动配置)
当用户要求「配置 Get笔记」「连接 Get笔记」时,使用此流程自动获取凭证。
### 流程概述
1. **申请授权码** → 获取 `code` 和 `verification_uri`
2. **展示给用户** → 发送授权链接
3. **轮询等待** → 用户授权后获取 `api_key` 和 `client_id`
4. **写入配置** → 自动配置完成
### 步骤 1:申请授权码
```
POST https://openapi.biji.com/open/api/v1/oauth/device/code
Content-Type: application/json
```
请求体:
```json
{
"client_id": "cli_a1b2c3d4e5f6789012345678abcdef90"
}
```
> ⚠️ **client_id 固定为 `cli_a1b2c3d4e5f6789012345678abcdef90`**,这是 Get笔记 为 OpenClaw 预注册的应用。
返回:
```json
{
"success": true,
"data": {
"code": "abc123...",
"verification_uri": "https://biji.com/openapi/oauth/authorize?code=abc123...",
"user_code": "ABCD-1234",
"expires_in": 600,
"interval": 5
}
}
```
| 字段 | 说明 |
|------|------|
| code | 授权码,轮询时使用 |
| verification_uri | 授权链接,**发送给用户点击** |
| user_code | 确认码,**必须展示给用户核对** |
| expires_in | 授权码有效期(秒),默认 600 |
| interval | 建议轮询间隔(秒),默认 5 |
### 步骤 2:展示授权链接
将 `verification_uri` 和 `user_code` 发送给用户:
> 🔗 请点击链接完成授权:
>
> {verification_uri}
>
> ⚠️ **请核对确认码**:`{user_code}`
>
> 授权页面会显示确认码,请确保与上面一致后再点击授权。授权码 10 分钟内有效。
**安全提醒**:`user_code` 用于防止钓鱼攻击。用户在授权页面看到的确认码必须与 Agent 展示的一致,不一致请勿授权。
**发送后立即启动后台轮询**(步骤 3)。
### 步骤 3:轮询等待授权
发送授权链接给用户后,**立即在后台启动轮询**,无需等待用户回复。
```
POST https://openapi.biji.com/open/api/v1/oauth/token
Content-Type: application/json
```
请求体:
```json
{
"grant_type": "device_code",
"client_id": "cli_a1b2c3d4e5f6789012345678abcdef90",
"code": "{code}"
}
```
**轮询策略**:
- **间隔**:5 秒查询一次
- **超时**:最多轮询 10 分钟(与授权码有效期一致)
- **并行**:轮询在后台进行,不阻塞用户其他操作
**推荐:使用轮询脚本**
```bash
# 方式 1:后台轮询 + process poll 等待结果(OpenClaw 推荐)
# exec 启动后台任务,然后用 process poll 等待完成
exec: python scripts/oauth_poll.py "{code}"
background: true
# 用 process poll 等待结果(最长等 10 分钟)
process: poll
sessionId: {上一步返回的 sessionId}
timeout: 600000
# 方式 2:简单等待(适合短时间)
result=$(python scripts/oauth_poll.py "{code}")
api_key=$(echo "$result" | jq -r '.api_key')
client_id=$(echo "$result" | jq -r '.client_id')
```
脚本会自动处理各种状态,成功时输出 JSON,失败时输出错误到 stderr。
**轮询响应状态**:
| 响应 | 说明 | 处理方式 |
|------|------|---------|
| `{"msg": "authorization_pending"}` | 用户尚未操作 | 继续轮询 |
| `{"msg": "rejected"}` | 用户拒绝授权 | **停止轮询**,告知用户已拒绝 |
| `{"msg": "expired_token"}` | 授权码已过期 | **停止轮询**,提示重新发起 |
| `{"msg": "already_consumed"}` | 授权码已使用 | **停止轮询**,可能已配置成功 |
| `{"api_key": "...", "client_id": "...", ...}` | **授权成功** | 进入步骤 4 |
**授权成功返回**:
```json
{
"success": true,
"data": {
"client_id": "cli_a1b2c3d4e5f6789012345678abcdef90",
"api_key": "gk_live_xxx",
"key_id": "abc123",
"expires_at": 1742000000
}
}
```
| 字段 | 说明 |
|------|------|
| client_id | 应用 ID |
| api_key | API Key,**写入配置文件** |
| key_id | Key ID(管理用) |
| expires_at | 过期时间戳(Unix 秒),有效期 1 年 |
### 步骤 4:写入配置
将获取的凭证写入 `~/.openclaw/openclaw.json`:
```json
{
"skills": {
"entries": {
"getnote": {
"apiKey": "{api_key}",
"env": {
"GETNOTE_CLIENT_ID": "{client_id}"
}
}
}
}
}
```
**告知用户**:
> ✅ Get笔记 配置完成!
>
> - API Key 有效期至 {expires_at 格式化日期}
> - 现在可以使用「记一下」「查笔记」等功能了
> - 如需注销授权,请访问:https://www.biji.com/openapi?tab=keys
FILE:README.md
# Get笔记 Skill
[](https://opensource.org/licenses/MIT-0)
让 AI 成为你的第二大脑。随时记录,需要时召回。
---
## ✨ 核心能力
| 能力 | 说明 |
|------|------|
| **📎 一键保存链接** | 发个链接就能保存,自动抓取原文、生成摘要。支持小宇宙播客、小红书、微信公众号、B站、抖音及任意公开网页 |
| **🖼 图片秒变笔记** | 发张图片自动 OCR 识别文字、AI 分析图片内容 |
| **🔍 语义搜索** | 不用翻全部笔记,直接搜关键词,AI 语义召回相关内容 |
| **📚 知识库管理** | 用知识库和标签整理笔记,还能订阅博主、获取直播总结 |
| **🎤 语音笔记原文** | 录音笔记支持获取完整转写文本 |
---
## 💡 使用场景
### ✏️ 随手记录
**通勤路上想到一个点子**
> 👤 记一下笔记:支付流程可以加一个进度条,用户等待时不焦虑
>
> 🤖 已记录,自动打上「产品优化」标签。
**听播客时有感触**
> 👤 刚听到一个观点挺好的,记一下:好的产品是让用户少做选择,不是多做选择
>
> 🤖 已记录。
**开完会**
> 👤 帮我记下刚才会上定的几件事:1. 下周三前完成设计稿 2. 找小王对接接口 3. 周五前给老板汇报进度
>
> 🤖 已记录。
---
### 🔍 要用时召回
**写周报时**
> 👤 帮我找找这周我记过的工作相关的东西
>
> 🤖 找到 5 条相关笔记:周一你记了客户反馈的问题、周三记了技术方案讨论、周四......
**被领导问到**
> 👤 上次我们讨论过用户分层的事,当时怎么说的来着?
>
> 🤖 11月8号你记过:高价值用户定义为月消费超过500元,占比约12%......
**写文章找素材**
> 👤 我想写篇关于做产品的文章,帮我找找我之前的相关想法
>
> 🤖 找到 8 条相关笔记,已按时间排序整理给你。
---
### 🔗 保存链接和图片
**看到好文章**
> 👤 存到笔记 https://example.com/article
>
> 🤖 链接已提交,正在抓取分析中...
> 🤖 搞定 ✓ 已保存:《如何做好产品设计》
**拍到有用的东西**
> 👤 [发送图片]
>
> 🤖 存到笔记?
>
> 👤 对
>
> 🤖 搞定 ✓ 图片已保存。
**探店记录**
> 👤 记一下,这家店叫船歌鱼水饺,招牌是鲅鱼饺子,人均80
>
> 🤖 已记录,打上「美食」标签。
---
## 📦 安装
### 方式一:通过 ClawHub 安装(推荐)
```bash
clawhub install getnote
```
### 方式二:让 AI 助手安装
> 帮我安装 Get笔记 skill,地址是 https://raw.githubusercontent.com/iswalle/getnote-openclaw/main/SKILL.md
### 方式三:手动安装
```bash
mkdir -p ~/.openclaw/workspace/skills/getnote
cd ~/.openclaw/workspace/skills/getnote
curl -sL https://raw.githubusercontent.com/iswalle/getnote-openclaw/main/SKILL.md -o SKILL.md
curl -sL https://raw.githubusercontent.com/iswalle/getnote-openclaw/main/package.json -o package.json
```
---
## 🔑 配置
### 自动配置(默认)
安装后首次使用时,AI 会自动发起 OAuth 授权:
1. 你说「存到笔记」或任何笔记相关操作
2. AI 检测到未配置,自动生成授权链接
3. 点击链接,授权
4. 自动配置完成,继续执行你的请求
无需手动配置,无需记忆任何命令。
### 手动配置(可选)
1. 前往 **[Get笔记开放平台](https://www.biji.com/openapi)** 获取 API Key 和 Client ID
2. 在 `~/.openclaw/openclaw.json` 中添加:
```json
{
"skills": {
"entries": {
"getnote": {
"apiKey": "gk_live_xxx",
"env": {
"GETNOTE_CLIENT_ID": "cli_xxx",
"GETNOTE_OWNER_ID": "ou_xxx"
}
}
}
}
}
```
> 💡 `GETNOTE_OWNER_ID` 可选,配置后只有你能操作笔记(群聊安全)
> 💡 需要 [Get笔记会员](https://www.biji.com/checkout?product_alias=6AydVpYeKl) 才能使用 API
---
## 🔐 安全说明
> ⚠️ **隐私保护**:笔记是你的私密数据,AI 会严格校验身份。
- 配置 `GETNOTE_OWNER_ID` 后,只有你能操作笔记
- 群聊中其他人无法通过 AI 读取你的笔记
- **不要在聊天中发送 API Key**,请手动配置到环境变量
---
## 🛠 支持的笔记类型
| 类型 | 说明 | 支持 |
|------|------|------|
| `plain_text` | 纯文本笔记 | ✅ 读写 |
| `link` | 链接笔记(自动抓取正文) | ✅ 读写 |
| `img_text` | 图片笔记 | ✅ 读写 |
| `audio` | 即时录音 | 📖 仅读取 |
| `meeting` | 会议录音 | 📖 仅读取 |
| `local_audio` | 本地音频 | 📖 仅读取 |
| `internal_record` | 内录音频 | 📖 仅读取 |
| `class_audio` | 课堂录音 | 📖 仅读取 |
| `recorder_audio` | 录音卡长录 | 📖 仅读取 |
| `recorder_flash_audio` | 录音卡闪念 | 📖 仅读取 |
> 💡 语音类笔记可读取 AI 摘要和转写原文,需调用详情接口获取。
---
## 📜 相关链接
- [Get笔记官网](https://biji.com)
- [开放平台](https://www.biji.com/openapi)
- [ClawHub](https://clawhub.ai/iswalle/getnote)
- [开通会员](https://www.biji.com/checkout?product_alias=6AydVpYeKl)
---
## License
MIT-0 (MIT No Attribution) · Published on [ClawHub](https://clawhub.ai)
FILE:_meta.json
{
"ownerId": "kn75rbp8nyw5q0xmrzg01nfeq5829kd0",
"slug": "lovefromio-getnote",
"version": "1.5.2",
"publishedAt": 1774353244832
}
FILE:package.json
{
"name": "getnote",
"version": "1.5.2",
"description": "让 AI 帮你保存和管理 Get笔记。有灵感,随时保存,发个链接说「存一下」,发张图片说「记到笔记」,就这么简单。",
"license": "MIT-0",
"author": "[email protected]",
"homepage": "https://biji.com",
"repository": {
"type": "git",
"url": "https://github.com/iswalle/getnote-openclaw.git"
},
"bugs": {
"url": "https://github.com/iswalle/getnote-openclaw/issues"
},
"keywords": [
"notes",
"get笔记",
"笔记",
"录音",
"会议纪要",
"知识库",
"ai",
"openclaw",
"skill"
],
"clawhub": {
"acceptLicenseTerms": true
},
"openclaw": {
"minVersion": "0.1.0",
"baseUrl": "https://openapi.biji.com",
"requires": {},
"optionalEnv": [
"GETNOTE_API_KEY",
"GETNOTE_CLIENT_ID",
"GETNOTE_OWNER_ID"
]
}
}
FILE:references/api-details.md
# Get笔记 API 详细参考
## 目录
1. [错误码表](#错误码表)
2. [error.reason 取值表](#errorreason-取值表)
3. [限流响应结构](#限流响应结构)
4. [笔记详情独有字段](#笔记详情独有字段)
5. [新建笔记字段说明](#新建笔记字段说明)
6. [图片上传凭证返回字段](#图片上传凭证返回字段)
---
## 错误码表
| 错误码 | 说明 |
|--------|------|
| 10000 | 参数错误 |
| 10001 | 鉴权失败 |
| 10201 | 非会员 |
| 20001 | 笔记不存在 |
| 30000 | 服务调用失败 |
| 42900 | 限流 |
| 50000 | 系统错误 |
---
## error.reason 取值表
| reason | 说明 |
|--------|------|
| not_member | 非会员,引导开通:https://www.biji.com/checkout?product_alias=6AydVpYeKl |
| qps_global | 全局 QPS 超限 |
| qps_bucket | 桶级 QPS 超限 |
| quota_day | 当日配额用尽 |
| quota_month | 当月配额用尽 |
---
## 限流响应结构
429 错误时,`error` 字段中包含 `rate_limit` 对象:
```json
{
"success": false,
"error": {
"code": 42900,
"message": "rate limited",
"reason": "quota_day",
"rate_limit": {
"read": {
"daily": {"limit": 1000, "used": 1000, "remaining": 0, "reset_at": 1741190400},
"monthly": {"limit": 10000, "used": 3000, "remaining": 7000, "reset_at": 1743811200}
},
"write": {
"daily": {"limit": 200, "used": 200, "remaining": 0, "reset_at": 1741190400},
"monthly": {"limit": 2000, "used": 600, "remaining": 1400, "reset_at": 1743811200}
},
"write_note": {
"daily": {"limit": 50, "used": 50, "remaining": 0, "reset_at": 1741190400},
"monthly": {"limit": 500, "used": 150, "remaining": 350, "reset_at": 1743811200}
}
}
},
"request_id": "xxx"
}
```
字段说明:
- `limit` - 配额上限
- `used` - 已使用量
- `remaining` - 剩余量
- `reset_at` - 重置时间(Unix 时间戳,秒)
---
## 笔记详情独有字段
调用 `GET /open/api/v1/resource/note/detail?id={note_id}` 时返回,列表接口不包含这些字段:
| 字段 | 类型 | 说明 |
|------|------|------|
| `audio.original` | string | 语音转写原文 |
| `audio.play_url` | string | 音频播放地址(有效期限制) |
| `audio.duration` | int | 音频时长(秒) |
| `web_page.content` | string | 链接网页完整原文 |
| `web_page.url` | string | 原始链接地址 |
| `web_page.excerpt` | string | AI 生成的摘要 |
| `attachments[]` | array | 附件列表 |
| `attachments[].type` | string | 附件类型:audio \| image \| link \| pdf |
---
## 新建笔记字段说明
`POST /open/api/v1/resource/note/save` 请求体字段:
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `title` | string | 否 | 笔记标题 |
| `content` | string | 否 | Markdown 格式正文 |
| `note_type` | string | 否 | 笔记类型,默认 `plain_text`:<br>• `plain_text` - 纯文本(同步)<br>• `link` - 链接笔记(异步,须轮询)<br>• `img_text` - 图片笔记(异步,须轮询) |
| `tags` | string[] | 否 | 标签名称列表 |
| `parent_id` | int64 | 否 | 父笔记 ID,创建子笔记时填写,默认 0 |
| `link_url` | string | link 类型必填 | 要保存的链接 URL |
| `image_urls` | string[] | img_text 类型必填 | 图片访问地址列表,使用上传凭证中的 `access_url` |
**返回说明**:
- `plain_text`:同步返回,`data.note_id` 即为笔记 ID
- `link` / `img_text`:返回 `data.task_id`,需轮询 `/task/progress` 至 `status=success` 或 `failed`
---
## 图片上传凭证返回字段
`GET /open/api/v1/resource/image/upload_token?mime_type=jpg&count=1` 返回 `data.tokens[]`,每个 token 包含:
| 字段 | 说明 |
|------|------|
| `host` | OSS 上传地址(POST 目标 URL) |
| `object_key` | 文件在 OSS 上的路径 |
| `accessid` | OSS AccessKey ID |
| `policy` | 上传策略(Base64 编码) |
| `signature` | 请求签名 |
| `callback` | 回调参数(Base64 编码,必须包含在上传请求中) |
| `access_url` | 上传成功后的文件访问地址,**创建图片笔记时填入 `image_urls`** |
| `oss_content_type` | 上传时需设置的 Content-Type(与 mime_type 对应) |
⚠️ `mime_type` 必须与实际文件格式一致,否则 OSS 签名验证失败。
---
## Scope 权限列表
| Scope | 说明 |
|-------|------|
| note.content.read | 读取笔记(列表、详情)|
| note.content.write | 添加或修改笔记 |
| note.tag.read | 获取标签列表 |
| note.tag.write | 修改标签 |
| note.topic.read | 获取知识库笔记 |
| note.topic.write | 笔记加入或移除知识库 |
| topic.read | 获取知识库信息 |
| topic.write | 创建和编辑知识库 |
| note.recall.read | 笔记内容搜索 |
| note.topic.recall.read | 知识库内容搜索 |
| note.image.upload | 获取上传图片签名 |
| note.content.trash | 笔记移入回收站 |
| topic.blogger.read | 获取知识库订阅博主 |
| topic.live.read | 获取知识库订阅直播 |
---
## 博主内容字段说明
| 字段 | 说明 |
|------|------|
| post_id_alias | 内容 ID,查详情时必用 |
| post_name | 内容名称(原标题)|
| post_type | 类型:video / audio / article / live |
| post_cover | 封面图 |
| post_title | AI 生成标题 |
| post_summary | AI 摘要(Markdown)|
| post_url | 原文链接 |
| post_icon | 博主头像 |
| post_subtitle | 副标题 |
| post_media_text | 原文内容(仅详情接口返回)|
| post_create_time | 创建时间(YYYY-MM-DD HH:MM:SS)|
| post_publish_time | 发布时间(YYYY-MM-DD HH:MM:SS)|
---
## 直播字段说明
| 字段 | 说明 |
|------|------|
| live_id | 直播 ID,查直播详情时必用 |
| follow_id | 订阅关系 ID |
| name | 直播名称 |
| cover | 封面图 |
| sub_title | 副标题 |
| link | 直播链接 |
| platform | 平台(如 DEDAO)|
| status | 直播状态(已结束为 FINISHED)|
| follow_time | 订阅时间(YYYY-MM-DD HH:MM:SS)|
---
## 召回结果字段说明
### 笔记召回 (recall) 返回字段
| 字段 | 说明 |
|------|------|
| id | 笔记 ID |
| note_id | 笔记 ID(字符串格式)|
| title | 标题 |
| content | 摘要内容 |
| note_type | 笔记类型 |
| tags | 标签列表 |
| created_at | 创建时间 |
### 知识库召回 (recall/knowledge) 返回字段
| 字段 | 说明 |
|------|------|
| id | 内容 ID |
| content_type | 内容类型:note / blogger / live |
| title | 标题 |
| summary | 摘要 |
| source_type | 来源类型 |
| created_at | 创建时间 |
---
## OSS 上传详细示例
```bash
# 1. 获取上传凭证
curl 'https://openapi.biji.com/open/api/v1/resource/image/upload_token?mime_type=jpg&count=1' \
-H 'X-Client-ID: {client_id}' \
-H 'Authorization: {api_key}'
# 2. 上传到 OSS(使用返回的凭证)
curl -X POST '{host}' \
-F 'key={object_key}' \
-F 'policy={policy}' \
-F 'OSSAccessKeyId={accessid}' \
-F 'signature={signature}' \
-F 'callback={callback}' \
-F 'Content-Type=image/jpeg' \
-F 'file=@/path/to/image.jpg'
# 3. 创建图片笔记
curl -X POST 'https://openapi.biji.com/open/api/v1/resource/note/save' \
-H 'X-Client-ID: {client_id}' \
-H 'Authorization: {api_key}' \
-H 'Content-Type: application/json' \
-d '{"note_type": "img_text", "content": "描述", "image_urls": ["{access_url}"]}'
```
FILE:scripts/oauth_poll.py
#!/usr/bin/env python3
"""
Get笔记 OAuth 授权轮询脚本
用法:
python oauth_poll.py <code> [client_id]
参数:
code - 授权码(从 /oauth/device/code 获取的 code 字段)
client_id - 应用 ID(可选,默认: cli_a1b2c3d4e5f6789012345678abcdef90)
返回:
成功: 输出 JSON {"api_key": "...", "client_id": "...", "key_id": "...", "expires_at": ...}
失败: 输出错误信息到 stderr,退出非零状态码
退出码:
0 - 授权成功
2 - 用户拒绝授权
3 - 授权码已过期
4 - 授权码已被使用
5 - 未知错误
6 - 轮询超时
示例:
result=$(python oauth_poll.py "abc123...")
api_key=$(echo "$result" | jq -r '.api_key')
"""
import sys
import time
import json
import urllib.request
import urllib.error
API_URL = "https://openapi.biji.com/open/api/v1/oauth/token"
DEFAULT_CLIENT_ID = "cli_a1b2c3d4e5f6789012345678abcdef90"
INTERVAL = 5 # 轮询间隔(秒)
MAX_ATTEMPTS = 120 # 最大尝试次数(5秒 * 120 = 10分钟)
def poll_token(code: str, client_id: str) -> dict:
"""轮询授权状态,返回授权结果或抛出异常"""
payload = json.dumps({
"grant_type": "device_code",
"client_id": client_id,
"code": code
}).encode('utf-8')
headers = {"Content-Type": "application/json"}
for attempt in range(MAX_ATTEMPTS):
try:
req = urllib.request.Request(API_URL, data=payload, headers=headers, method='POST')
with urllib.request.urlopen(req, timeout=30) as resp:
data = json.loads(resp.read().decode('utf-8'))
except urllib.error.URLError as e:
print(f"网络错误: {e}", file=sys.stderr)
time.sleep(INTERVAL)
continue
# 检查是否授权成功
if data.get("success") and data.get("data", {}).get("api_key"):
return data["data"]
# 检查状态消息
msg = data.get("data", {}).get("msg", "")
if msg == "authorization_pending":
time.sleep(INTERVAL)
continue
elif msg == "rejected":
print("用户拒绝了授权", file=sys.stderr)
sys.exit(2)
elif msg == "expired_token":
print("授权码已过期,请重新发起", file=sys.stderr)
sys.exit(3)
elif msg == "already_consumed":
print("授权码已被使用", file=sys.stderr)
sys.exit(4)
else:
print(f"未知响应: {data}", file=sys.stderr)
sys.exit(5)
print("轮询超时(10分钟),请重新发起授权", file=sys.stderr)
sys.exit(6)
def main():
if len(sys.argv) < 2:
print(f"用法: {sys.argv[0]} <code> [client_id]", file=sys.stderr)
sys.exit(1)
code = sys.argv[1]
client_id = sys.argv[2] if len(sys.argv) > 2 else DEFAULT_CLIENT_ID
result = poll_token(code, client_id)
print(json.dumps(result))
if __name__ == "__main__":
main()
FILE:scripts/upload_image.py
#!/usr/bin/env python3
"""
Get笔记图片上传脚本
用法:
python upload_image.py <图片路径> [--api-key KEY] [--client-id ID]
示例:
python upload_image.py /path/to/image.jpg
python upload_image.py /path/to/image.jpg --api-key gk_live_xxx --client-id cli_xxx
环境变量:
GETNOTE_API_KEY - API Key(如未提供 --api-key 则使用此变量)
GETNOTE_CLIENT_ID - Client ID(可选)
"""
import os
import sys
import argparse
import mimetypes
from pathlib import Path
try:
import requests
except ImportError:
print("错误: 需要安装 requests 库")
print("运行: pip install requests")
sys.exit(1)
BASE_URL = "https://openapi.biji.com/open/api/v1/resource"
DEFAULT_CLIENT_ID = "cli_a1b2c3d4e5f6789012345678abcdef90"
def get_mime_type(file_path: str) -> str:
"""获取文件 MIME 类型"""
mime_type, _ = mimetypes.guess_type(file_path)
return mime_type or "image/jpeg"
def upload_image(image_path: str, api_key: str, client_id: str = DEFAULT_CLIENT_ID) -> str:
"""
上传图片到 Get笔记 OSS
Args:
image_path: 本地图片路径
api_key: Get笔记 API Key
client_id: 应用 Client ID
Returns:
上传后的访问 URL
"""
# 检查文件
if not os.path.exists(image_path):
raise FileNotFoundError(f"文件不存在: {image_path}")
mime_type = get_mime_type(image_path)
headers = {
"Authorization": api_key,
"X-Client-ID": client_id
}
# 步骤 1: 获取上传凭证
print("[1/3] 获取上传凭证...")
resp = requests.get(
f"{BASE_URL}/image/upload_token",
params={"count": 1, "mime_type": mime_type},
headers=headers
)
resp.raise_for_status()
data = resp.json()
if not data.get("success"):
error = data.get("error", {})
raise Exception(f"获取凭证失败: {error.get('message', 'Unknown error')}")
# 兼容两种返回格式:tokens[] 数组 或 直接在 data 中
token_data = data["data"]
if "tokens" in token_data:
tokens = token_data["tokens"]
if not tokens:
raise Exception("获取凭证失败: tokens 为空")
token = tokens[0]
else:
# 直接在 data 中的格式
token = token_data
host = token["host"]
object_key = token["object_key"]
accessid = token["accessid"]
policy = token.get("policy", "")
signature = token.get("signature", "")
callback = token.get("callback", "")
access_url = token["access_url"]
oss_content_type = token.get("oss_content_type", mime_type)
print("✓ 凭证获取成功")
# 步骤 2: 上传到 OSS(multipart form,字段顺序必须严格遵守)
print("[2/3] 上传图片到 OSS...")
file_name = os.path.basename(image_path)
with open(image_path, "rb") as f:
# 字段顺序:key → OSSAccessKeyId → policy → signature → callback → Content-Type → file
form_data = [
("key", object_key),
("OSSAccessKeyId", accessid),
("policy", policy),
("signature", signature),
("callback", callback),
("Content-Type", oss_content_type),
]
files = {"file": (file_name, f, mime_type)}
resp = requests.post(host, data=form_data, files=files)
if resp.status_code == 429:
# 限流处理
try:
error_data = resp.json()
rate_limit = error_data.get("error", {}).get("rate_limit", {})
print(f"限流: {rate_limit}", file=sys.stderr)
except:
pass
raise Exception(f"上传失败: 限流 (HTTP 429),请稍后重试")
if resp.status_code not in (200, 204):
raise Exception(f"上传失败: HTTP {resp.status_code}")
print("✓ 上传成功")
# 步骤 3: 返回访问 URL
print("[3/3] 完成")
return access_url
def main():
parser = argparse.ArgumentParser(
description="Get笔记图片上传脚本",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
示例:
%(prog)s /path/to/image.jpg
%(prog)s /path/to/image.jpg --api-key gk_live_xxx
环境变量:
GETNOTE_API_KEY - API Key
"""
)
parser.add_argument("image_path", help="本地图片路径")
parser.add_argument("--api-key", "-k", help="API Key(或设置 GETNOTE_API_KEY 环境变量)")
parser.add_argument("--client-id", "-c", help="Client ID(可选,默认使用 OpenClaw 预注册应用)")
args = parser.parse_args()
# 获取 API Key
api_key = args.api_key or os.environ.get("GETNOTE_API_KEY")
if not api_key:
print("错误: 请提供 API Key(--api-key 参数或 GETNOTE_API_KEY 环境变量)")
sys.exit(1)
# 获取 Client ID
client_id = args.client_id or os.environ.get("GETNOTE_CLIENT_ID") or DEFAULT_CLIENT_ID
try:
image_url = upload_image(args.image_path, api_key, client_id)
print()
print("=" * 40)
print("图片上传成功!")
print("=" * 40)
print()
print(f"访问 URL: {image_url}")
print()
print("💡 创建图片笔记:")
print(f' curl -X POST "https://openapi.biji.com/open/api/v1/resource/note/save?task_id=..."')
print(f' -H "Authorization: $GETNOTE_API_KEY"')
print(f' -H "Content-Type: application/json"')
print(f' -d \'{{"type":"img_text","image_urls":["{image_url}"]}}\'')
except Exception as e:
print(f"错误: {e}")
sys.exit(1)
if __name__ == "__main__":
main()
Talk to your Garmin data naturally - "what was my fastest speed snowboarding?", "how did I sleep last night?", "what was my heart rate at 3pm?". Access 20+ m...
---
name: garmin-health-analysis
description: Talk to your Garmin data naturally - "what was my fastest speed snowboarding?", "how did I sleep last night?", "what was my heart rate at 3pm?". Access 20+ metrics (sleep stages, Body Battery, HRV, VO2 max, training readiness, body composition, SPO2), download FIT/GPX files for route analysis, query elevation/pace at any point, and generate interactive health dashboards. From casual "show me this week's workouts" to deep "analyze my recovery vs training load".
version: 1.2.2
author: EversonL & Claude
homepage: https://github.com/eversonl/ClawdBot-garmin-health-analysis
metadata: {"clawdbot":{"emoji":"⌚","requires":{"env":["GARMIN_EMAIL","GARMIN_PASSWORD"]},"install":[{"id":"garminconnect","kind":"python","package":"garminconnect","label":"Install garminconnect (pip)"},{"id":"fitparse","kind":"python","package":"fitparse","label":"Install fitparse (pip)"},{"id":"gpxpy","kind":"python","package":"gpxpy","label":"Install gpxpy (pip)"}]}}
---
# Garmin Health Analysis
Query health metrics from Garmin Connect and generate interactive HTML charts.
## Two Installation Paths
This skill supports **two different setups**:
1. **Clawdbot Skill** (this guide) - Use with Clawdbot for automation and proactive health monitoring
2. **MCP Server** ([see MCP setup guide](references/mcp_setup.md)) - Use with standard Claude Desktop as an MCP server
Choose the path that matches your use case. You can also use both simultaneously!
---
## Clawdbot Skill Setup (first time only)
### 1. Install Dependencies
```bash
pip3 install garminconnect
```
### 2. Configure Credentials
You have three options to provide your Garmin Connect credentials:
#### Option A: Clawdbot Config (Recommended - UI configurable)
Add credentials to `~/.clawdbot/clawdbot.json`:
```json
{
"skills": {
"entries": {
"garmin-health-analysis": {
"enabled": true,
"env": {
"GARMIN_EMAIL": "[email protected]",
"GARMIN_PASSWORD": "your-password"
}
}
}
}
}
```
**Tip**: You can also set these through the Clawdbot UI in the Skills settings panel.
#### Option B: Local Config File
Create a config file in the skill directory:
```bash
cd ~/.clawdbot/skills/garmin-health-analysis
# or: cd <workspace>/skills/garmin-health-analysis
cp config.example.json config.json
# Edit config.json and add your email and password
```
**config.json:**
```json
{
"email": "[email protected]",
"password": "your-password"
}
```
**Note**: `config.json` is gitignored to keep your credentials secure.
#### Option C: Command Line
Pass credentials directly when authenticating:
```bash
python3 scripts/garmin_auth.py login \
--email [email protected] \
--password YOUR_PASSWORD
```
### 3. Authenticate
Login to Garmin Connect and save session tokens:
```bash
python3 scripts/garmin_auth.py login
```
This uses credentials from (in priority order):
1. Command line arguments (`--email`, `--password`)
2. Local config file (`config.json`)
3. Environment variables (`GARMIN_EMAIL`, `GARMIN_PASSWORD`)
4. Clawdbot config (`skills.entries.garmin-health-analysis.env`)
Session tokens are stored in `~/.clawdbot/garmin-tokens.json` and auto-refresh.
Check authentication status:
```bash
python3 scripts/garmin_auth.py status
```
## Fetching Data
Use `scripts/garmin_data.py` to get JSON data:
```bash
# Sleep (last 7 days default)
python3 scripts/garmin_data.py sleep --days 14
# Body Battery (Garmin's recovery metric)
python3 scripts/garmin_data.py body_battery --days 30
# HRV data
python3 scripts/garmin_data.py hrv --days 30
# Heart rate (resting, max, min)
python3 scripts/garmin_data.py heart_rate --days 7
# Activities/workouts
python3 scripts/garmin_data.py activities --days 30
# Stress levels
python3 scripts/garmin_data.py stress --days 7
# Combined summary with averages
python3 scripts/garmin_data.py summary --days 7
# Custom date range
python3 scripts/garmin_data.py sleep --start 2026-01-01 --end 2026-01-15
# User profile
python3 scripts/garmin_data.py profile
```
Output is JSON to stdout. Parse it to answer user questions.
## Generating Charts
Use `scripts/garmin_chart.py` for interactive HTML visualizations:
```bash
# Sleep analysis (hours + scores)
python3 scripts/garmin_chart.py sleep --days 30
# Body Battery recovery chart (color-coded)
python3 scripts/garmin_chart.py body_battery --days 30
# HRV & resting heart rate trends
python3 scripts/garmin_chart.py hrv --days 90
# Activities summary (by type, calories)
python3 scripts/garmin_chart.py activities --days 30
# Full dashboard (all 4 charts)
python3 scripts/garmin_chart.py dashboard --days 30
# Save to specific file
python3 scripts/garmin_chart.py dashboard --days 90 --output ~/Desktop/garmin-health.html
```
Charts open automatically in the default browser. They use Chart.js with a modern gradient design, stat cards, and interactive tooltips.
## Answering Questions
| User asks | Action |
|-----------|--------|
| "How did I sleep last night?" | `garmin_data.py summary --days 1`, report sleep hours + score |
| "How's my recovery this week?" | `garmin_data.py body_battery --days 7`, report average + trend |
| "Show me my health for the last month" | `garmin_chart.py dashboard --days 30` |
| "Is my HRV improving?" | `garmin_data.py hrv --days 30`, analyze trend |
| "What workouts did I do this week?" | `garmin_data.py activities --days 7`, list activities with details |
| "How's my resting heart rate?" | `garmin_data.py heart_rate --days 7`, report average + trend |
## Key Metrics
### Body Battery (0-100)
Garmin's proprietary recovery metric based on HRV, stress, sleep, and activity:
- **High (75-100)**: Fully recharged, ready for high intensity
- **Medium (50-74)**: Moderate energy, good for regular activity
- **Low (25-49)**: Limited energy, recovery needed
- **Very Low (0-24)**: Depleted, prioritize rest
### Sleep Scores (0-100)
Overall sleep quality based on duration, stages, and disturbances:
- **Excellent (90-100)**: Optimal restorative sleep
- **Good (80-89)**: Quality sleep with minor issues
- **Fair (60-79)**: Adequate but could improve
- **Poor (0-59)**: Significant sleep deficiencies
### HRV (Heart Rate Variability)
Measured in milliseconds, higher is generally better:
- Indicates nervous system balance and recovery capacity
- Track **trends** over time (increasing = improving recovery)
- Affected by sleep, stress, training load, illness
- Normal range varies by individual (20-200+ ms)
### Resting Heart Rate (bpm)
Lower generally indicates better cardiovascular fitness:
- **Athletes**: 40-60 bpm
- **Fit adults**: 60-70 bpm
- **Average adults**: 70-80 bpm
- Sudden increases may indicate stress, illness, or overtraining
### Stress Levels
Based on HRV analysis throughout the day:
- **Low stress**: Rest and recovery periods
- **Medium stress**: Normal daily activities
- **High stress**: Physical activity or mental pressure
## Health Analysis
When users ask for insights or want to understand their trends, use `references/health_analysis.md` for:
- Science-backed interpretation of all metrics
- Normal ranges by age and fitness level
- Pattern detection (weekly trends, recovery cycles, training load balance)
- Actionable recommendations based on data
- Warning signs that suggest rest or medical consultation
### Analysis workflow
1. Fetch data: `python3 scripts/garmin_data.py summary --days N`
2. Read `references/health_analysis.md` for interpretation framework
3. Apply the analysis framework: Status → Trends → Patterns → Insights → Recommendations
4. Always include disclaimer that this is informational, not medical advice
## Troubleshooting
### Authentication Issues
- **"Invalid credentials"**: Double-check email/password, try logging into Garmin Connect web
- **"Tokens expired"**: Run login again: `python3 scripts/garmin_auth.py login ...`
- **"Too many requests"**: Garmin rate-limits; wait a few minutes and try again
### Missing Data
- Some metrics require specific Garmin devices (Body Battery needs HRV-capable devices)
- Historical data may have gaps if device wasn't worn
- New accounts may have limited history
### Library Issues
- If `garminconnect` import fails: `pip3 install --upgrade garminconnect`
- Garmin occasionally changes their API; update the library if requests fail
## Privacy Note
- Credentials are stored locally in `~/.clawdbot/garmin-tokens.json`
- Session tokens refresh automatically
- No data is sent anywhere except to Garmin's official servers
- You can revoke access anytime by deleting the tokens file
## Comparison: Garmin vs Whoop
| Feature | Garmin | Whoop |
|---------|--------|-------|
| **Recovery metric** | Body Battery (0-100) | Recovery Score (0-100%) |
| **HRV tracking** | Yes (nightly average) | Yes (detailed) |
| **Sleep stages** | Light, Deep, REM, Awake | Light, SWS, REM, Awake |
| **Activity tracking** | Built-in GPS, many sport modes | Strain score (0-21) |
| **Stress** | All-day stress levels | Not directly tracked |
| **API** | Unofficial (garminconnect) | Official OAuth |
| **Device types** | Watches, fitness trackers | Wearable band only |
## References
- `references/api.md` — Garmin Connect API details (unofficial)
- `references/health_analysis.md` — Science-backed health data interpretation
- [garminconnect library](https://github.com/cyberjunky/python-garminconnect) — Python API wrapper
- [Garmin Connect](https://connect.garmin.com) — Official web interface
## Version Info
- **Created**: 2026-01-25
- **Author**: EversonL & Claude
- **Version**: 1.2.0
- **Dependencies**: garminconnect, fitparse, gpxpy (Python libraries)
- **License**: MIT
FILE:CHANGELOG.md
# Changelog
## v1.2.2 (2026-01-26)
### 🧹 Repository Cleanup & Focus
**Removed old MCP server files** - Now using dedicated MCP server repo for Claude Desktop users:
- Removed `CLAUDE_DESKTOP.md` (old MCP setup docs)
- Removed `mcp_server.py` (old Python MCP server - 750+ lines)
- Removed `requirements.txt` (old MCP dependencies)
**Updated documentation for clarity:**
- README.md now clearly identifies this as the Clawdbot skill
- Added prominent callout directing Claude Desktop users to the dedicated MCP server repo
- Simplified `references/mcp_setup.md` to redirect to https://github.com/eversonl/garmin-health-mcp-server
- Updated file tree to reflect current structure
**New dedicated MCP server** for Claude Desktop users:
- Purpose-built Node.js MCP server at https://github.com/eversonl/garmin-health-mcp-server
- Uses modern @modelcontextprotocol/sdk
- Easy `npm install` + `npm run auth` setup
- Comprehensive installation guide and troubleshooting
**Both can coexist** - Shared authentication allows users to install both the Clawdbot skill and MCP server simultaneously.
**This repo is now focused exclusively on the Clawdbot skill** for automated health monitoring, scheduled reports, and proactive check-ins.
## v1.2.0 (2026-01-26)
### 🚀 Major Feature: MCP Server for Claude Desktop & Code
**NEW: Works with Claude Desktop, Claude Code, and any MCP client!**
- Added `mcp_server.py` - full MCP (Model Context Protocol) server implementation
- Exposes all Garmin tools to Claude Desktop, Claude Code, and other MCP-compatible clients
- New installation guide: `CLAUDE_DESKTOP.md` with step-by-step setup
- 14 MCP tools available:
- Health metrics (sleep, Body Battery, HRV, activities, heart rate)
- Time-based queries (HR/stress/BB at specific times)
- Extended metrics (training readiness, body composition, SPO2)
- Activity file downloads and analysis
- Updated README with Claude Desktop quick start
- Added `mcp` to requirements.txt
**Now you can use this skill with:**
- ✅ Clawdbot (original)
- ✅ Claude Desktop (new!)
- ✅ Claude Code / VS Code extension (new!)
- ✅ Any MCP-compatible client (new!)
## v1.1.5 (2026-01-25)
### Metadata
- Expanded ClawdHub description with conversational examples and "talk to your data" messaging
- Showcases real use cases: "what was my fastest speed?", activity analysis, recovery tracking
## v1.1.3 (2026-01-25)
### Documentation
- Updated Version Info section in SKILL.md with correct author and all dependencies
## v1.1.2 (2026-01-25)
### Metadata
- Updated description to highlight v1.1+ features (time queries, 20+ metrics, FIT/GPX analysis)
- Changed author to "EversonL & Claude"
## v1.1.1 (2026-01-25)
### Documentation
- Updated metadata with new dependencies (fitparse, gpxpy)
## v1.1.0 (2026-01-25)
### 🚀 Major Features
**Time-Based Queries**
- New `garmin_query.py` script for time-based questions
- Ask "what was my heart rate at 3pm?" and get instant answers
- Supports heart rate, stress, Body Battery, steps
- Flexible time parsing (12/24 hour formats)
**Extended Metrics** (`garmin_data_extended.py`)
- Training readiness & training status
- Body composition (weight, body fat %, muscle mass, BMI)
- Weight tracking over time
- SPO2 (blood oxygen saturation)
- Detailed respiration data
- Intraday steps, floors climbed
- Intensity minutes (vigorous/moderate activity)
- Hydration tracking
- Time-series stress data
- Max metrics (VO2 max, fitness age, endurance/hill scores)
- Intraday heart rate (all samples)
**Activity File Analysis** (`garmin_activity_files.py`)
- Download FIT/GPX/TCX files from activities
- Parse FIT files for GPS, elevation, HR, cadence, power
- Parse GPX files for route visualization
- Query data at specific distances ("what was my elevation at mile 2?")
- Query data at specific times during activities
- Comprehensive activity analysis & statistics
- Support for advanced use cases (route mapping, pace analysis, elevation profiles)
**Documentation**
- New `references/extended_capabilities.md` with comprehensive usage examples
- Updated dependencies (fitparse, gpxpy)
## v1.0.1 (2026-01-25)
### Bug Fixes
- Fixed sleep data extraction - properly parse nested `dailySleepDTO` object from Garmin API
- Sleep time and scores now display correctly in all charts and dashboards
## v1.0.0 (2026-01-25)
### Initial Release
**Features:**
- Fetch health data from Garmin Connect (sleep, Body Battery, HRV, heart rate, activities, stress)
- Generate interactive HTML charts with Chart.js
- Science-backed health analysis framework
- Support for multiple credential configuration methods
- Automatic token refresh
**Data Available:**
- Sleep: duration, stages (deep/light/REM), scores, HRV during sleep
- Body Battery: Garmin's recovery metric (0-100)
- HRV: nightly heart rate variability with baseline tracking
- Heart Rate: resting, max, min throughout the day
- Activities: workouts with calories, duration, heart rate, GPS data
- Stress: all-day stress levels based on HRV analysis
**Charts:**
- Sleep analysis (hours + scores)
- Body Battery recovery (color-coded by level)
- HRV & Resting Heart Rate trends
- Activities summary (by type with calories)
- Full dashboard combining all metrics
**Configuration:**
- UI-configurable via Clawdbot config (`skills.entries.garmin-health-analysis.env`)
- Local config.json support
- Command-line arguments
- Environment variables
**Requirements:**
- Python 3.7+
- garminconnect library (installed via pip)
- Garmin Connect account with wearable device
**Security:**
- Session tokens stored locally in `~/.clawdbot/garmin/`
- Tokens auto-refresh
- No data sent anywhere except Garmin's official servers
FILE:README.md
# Garmin Health Analysis - Clawdbot Skill
> **Talk to your Garmin data naturally** - "what was my fastest speed snowboarding?", "how did I sleep last night?", "what was my heart rate at 3pm?"
Access 20+ metrics from your Garmin device: sleep stages, Body Battery, HRV, VO2 max, training readiness, body composition, SPO2, and more. Download FIT/GPX files, query elevation/pace at any point, and generate interactive health dashboards.
## 🔵 Looking for Claude Desktop?
**This is the Clawdbot skill repo.** For standard Claude Desktop, use the dedicated MCP server:
👉 **[garmin-health-mcp-server](https://github.com/eversonl/garmin-health-mcp-server)** - Node.js MCP server for Claude Desktop
---
## 🚀 Clawdbot Installation
**Best for**: Automated health monitoring, scheduled reports, proactive check-ins
```bash
# Install via clawdhub
clawdhub install garmin-health-analysis
# Or manually
cd ~/.clawdbot/skills
git clone https://github.com/eversonl/ClawdBot-garmin-health-analysis.git garmin-health-analysis
# Install dependencies
pip3 install garminconnect fitparse gpxpy
# Configure credentials and authenticate
python3 scripts/garmin_auth.py login
```
**[📖 Full Setup Guide](SKILL.md)**
## ⚡ Features
- **Natural language queries**: "How's my recovery this week?" → instant Body Battery analysis
- **Sleep analysis**: Hours, stages (light/deep/REM), quality scores, trends
- **Recovery tracking**: Body Battery, HRV, training readiness, stress levels
- **Workout data**: Activities by type, calories, duration, pace, elevation
- **Health metrics**: Resting heart rate, VO2 max, body composition, SPO2
- **Activity files**: Download FIT/GPX for detailed route and performance analysis
- **Interactive charts**: Beautiful HTML dashboards with Chart.js visualizations
- **Science-backed insights**: Interpret trends with expert analysis framework
## 📊 Example Queries
**Clawdbot or Claude Desktop:**
> "How did I sleep last night?"
>
> "Show me my health dashboard for the last month"
>
> "Is my HRV improving?"
>
> "What was my fastest speed during yesterday's bike ride?"
>
> "How's my recovery vs. training load balance?"
>
> "Download the GPX file for my Sunday run"
## 🛠️ Key Metrics
| Metric | Range | What It Means |
|--------|-------|---------------|
| **Body Battery** | 0-100 | Garmin's recovery score (higher = more energy) |
| **Sleep Score** | 0-100 | Overall sleep quality (90+ = excellent) |
| **HRV** | 20-200+ ms | Heart rate variability (higher = better recovery) |
| **Resting HR** | 40-80 bpm | Lower is generally better (athletes: 40-60) |
| **Stress** | Low/Med/High | Based on HRV throughout the day |
## 📦 What's Included
```
garmin-health-analysis/
├── SKILL.md # Clawdbot setup & usage
├── README.md # This file
├── install.sh # Automated installation script
├── scripts/
│ ├── garmin_auth.py # Authentication helper
│ ├── garmin_data.py # Fetch health metrics (JSON)
│ ├── garmin_chart.py # Generate HTML charts
│ ├── garmin_data_extended.py # Extended metrics (VO2, readiness, etc.)
│ ├── garmin_activity_files.py # Download FIT/GPX files
│ └── garmin_query.py # Time-based queries
├── references/
│ ├── health_analysis.md # Metric interpretation guide
│ ├── api.md # Garmin Connect API docs
│ └── extended_capabilities.md # Advanced features
└── config.example.json # Credentials template
```
## 🔒 Privacy & Security
- Credentials stored locally (never sent to third parties)
- Session tokens auto-refresh (no repeated logins)
- Connects only to Garmin's official API
- No cloud storage or external data sharing
- Open source - audit the code yourself
## 📚 Documentation
- **[SKILL.md](SKILL.md)** - Complete Clawdbot setup, commands, troubleshooting
- **[references/health_analysis.md](references/health_analysis.md)** - Science-backed metric interpretation
- **[references/api.md](references/api.md)** - Garmin Connect API details
- **[references/extended_capabilities.md](references/extended_capabilities.md)** - Advanced features
### Looking for Claude Desktop?
See **[garmin-health-mcp-server](https://github.com/eversonl/garmin-health-mcp-server)** for the dedicated MCP server (you can use both!)
## 🐛 Troubleshooting
**Authentication issues?**
- Run `python3 scripts/garmin_auth.py login` to refresh tokens
- Check credentials in config.json or environment variables
- Try logging into Garmin Connect web to verify account
**Missing data?**
- Some metrics require specific devices (Body Battery needs HRV-capable watches)
- Check device was worn during the time period
- New accounts may have limited history
**Rate limits?**
- Garmin limits API requests - wait a few minutes and try again
- Batch queries when possible (use `summary` instead of individual calls)
## 🙏 Credits
- **Author**: EversonL & Claude
- **Version**: 1.2.0
- **License**: MIT
- **Dependencies**: [python-garminconnect](https://github.com/cyberjunky/python-garminconnect), fitparse, gpxpy
## 🔗 Links
- **Clawdbot**: [clawdbot.com](https://clawdbot.com)
- **ClawdHub**: [clawdhub.com](https://clawdhub.com)
- **Garmin Connect**: [connect.garmin.com](https://connect.garmin.com)
---
**Questions?** Open an issue on GitHub or ask in the Clawdbot Discord!
FILE:_meta.json
{
"ownerId": "kn79g0y3dssw91dcngn485stzd7zxmhs",
"slug": "lovefromio-garmin-health-analysis",
"version": "1.2.2",
"publishedAt": 1769387065409
}
FILE:config.example.json
{
"email": "[email protected]",
"password": "your-password"
}
FILE:config.json
{
"email": "[email protected]",
"password": "XSxs366496"
}
FILE:install.sh
#!/usr/bin/env bash
# Garmin Health Analysis - Clawdbot Skill Installation
# Run this after cloning the skill to your Clawdbot skills directory
set -e # Exit on error
echo "🏃 Installing Garmin Health Analysis Skill..."
echo
# Check Python
if ! command -v python3 &> /dev/null; then
echo "❌ Error: Python 3 is required but not found"
exit 1
fi
echo "✓ Python 3 found: $(python3 --version)"
# Install Python dependencies
echo
echo "📦 Installing Python dependencies..."
if pip3 install --user garminconnect fitparse gpxpy 2>/dev/null; then
echo "✓ Dependencies installed (--user)"
elif pip3 install --break-system-packages garminconnect fitparse gpxpy 2>/dev/null; then
echo "✓ Dependencies installed (--break-system-packages)"
elif pip3 install garminconnect fitparse gpxpy 2>/dev/null; then
echo "✓ Dependencies installed (system-wide)"
else
echo "❌ Failed to install Python dependencies"
echo " Try manually: pip3 install --user garminconnect fitparse gpxpy"
exit 1
fi
# Create config from example if it doesn't exist
if [ ! -f "config.json" ] && [ -f "config.example.json" ]; then
echo
echo "📝 Creating config.json from example..."
cp config.example.json config.json
echo "✓ config.json created (edit with your credentials)"
fi
# Success
echo
echo "✅ Installation complete!"
echo
echo "Next steps:"
echo " 1. Add your Garmin credentials:"
echo " - Edit config.json, or"
echo " - Set GARMIN_EMAIL and GARMIN_PASSWORD env vars, or"
echo " - Add to ~/.clawdbot/clawdbot.json skills config"
echo
echo " 2. Authenticate:"
echo " python3 scripts/garmin_auth.py login"
echo
echo " 3. Test:"
echo " python3 scripts/garmin_data.py summary --days 7"
echo
echo "📖 Read SKILL.md for full documentation"
FILE:references/api.md
# Garmin Connect API Reference (Unofficial)
This documents the unofficial Garmin Connect API accessed via the `garminconnect` Python library.
## Important Notes
- **Unofficial API**: Garmin does not provide a public API for personal use. This library reverse-engineers their web interface.
- **May break**: Garmin can change their API anytime, which may break the library
- **Rate limits**: Garmin implements rate limiting; excessive requests may temporarily block your account
- **Official alternative**: Garmin Health API exists for enterprise partnerships only
## Authentication
### Library: `garminconnect`
```bash
pip3 install garminconnect
```
### Authentication Flow
1. Login with email/password
2. Library handles OAuth token exchange
3. Tokens stored for session persistence
4. Tokens auto-refresh when expired
### Session Management
```python
from garminconnect import Garmin
# Initial login
client = Garmin(email, password)
client.login()
# Save tokens for reuse
oauth1 = client.garth.oauth1_token
oauth2 = client.garth.oauth2_token
# Restore session
client = Garmin()
client.garth.oauth1_token = oauth1
client.garth.oauth2_token = oauth2
```
## Core Endpoints
### User Profile
```python
# Full name
client.get_full_name()
# User summary (daily stats)
client.get_user_summary("2026-01-25")
```
### Sleep Data
```python
# Daily sleep details
client.get_sleep_data("2026-01-25")
```
**Response structure:**
```json
{
"sleepTimeSeconds": 28800, // Total sleep duration
"deepSleepSeconds": 7200, // Deep sleep
"lightSleepSeconds": 14400, // Light sleep
"remSleepSeconds": 7200, // REM sleep
"awakeSleepSeconds": 1800, // Awake time
"sleepScores": {
"overall": {"value": 85}, // Overall sleep score (0-100)
"duration": {...},
"quality": {...}
},
"restlessMoments": 12, // Number of restless periods
"avgSleepHeartRate": 52, // Average HR during sleep
"avgSleepHRV": 45, // Average HRV during sleep
"avgSleepRespiration": 14 // Respiration rate (breaths/min)
}
```
### HRV Data
```python
# Daily HRV summary
client.get_hrv_data("2026-01-25")
```
**Response structure:**
```json
{
"hrvSummary": {
"lastNightAvg": 45, // Last night's average HRV (ms)
"lastNight5MinHigh": 68, // 5-min high
"lastNight5MinLow": 28, // 5-min low
"weeklyAvg": 42, // 7-day rolling average
"baselineBalancedLow": 38, // Personal baseline low
"baselineBalancedHigh": 48, // Personal baseline high
"status": "BALANCED" // Status: BALANCED, UNBALANCED, POOR, LOW
}
}
```
### Body Battery
```python
# Daily Body Battery readings (time series)
client.get_body_battery("2026-01-25")
```
**Response structure:**
```json
[
{
"timestamp": 1737849600000, // Unix timestamp (ms)
"value": 85, // Body Battery level (0-100)
"charged": 45, // Amount charged overnight
"drained": 15 // Amount drained from previous
},
// ... more readings throughout the day
]
```
### Heart Rate
```python
# Daily heart rate summary
client.get_heart_rates("2026-01-25")
```
**Response structure:**
```json
{
"restingHeartRate": 52, // Resting HR (bpm)
"maxHeartRate": 165, // Max HR of the day
"minHeartRate": 48 // Min HR of the day
}
```
### Stress Levels
```python
# Daily stress data
client.get_stress_data("2026-01-25")
```
**Response structure:**
```json
{
"avgStressLevel": 35, // Average all-day stress (0-100)
"maxStressLevel": 78, // Peak stress
"restStressLevel": 15, // Stress during rest
"activityStressLevel": 65, // Stress during activity
"lowStressDuration": 14400, // Seconds in low stress (0-25)
"mediumStressDuration": 28800, // Seconds in medium stress (26-50)
"highStressDuration": 7200 // Seconds in high stress (51-100)
}
```
### Activities
```python
# Activities in date range
client.get_activities_by_date("2026-01-01", "2026-01-31", activitytype="")
```
**Response structure:**
```json
[
{
"activityId": 123456789,
"activityType": {
"typeKey": "running",
"typeId": 1
},
"activityName": "Morning Run",
"startTimeLocal": "2026-01-25 07:30:00",
"duration": 3600, // Duration in seconds
"distance": 10000, // Distance in meters
"calories": 650, // Calories burned
"averageHR": 152, // Average heart rate
"maxHR": 178, // Max heart rate
"elevationGain": 120, // Elevation gain (meters)
"averageSpeed": 2.78, // Speed (m/s)
"averageRunningCadence": 165 // Cadence (steps/min)
}
]
```
### Steps & Daily Totals
```python
# Daily stats summary
client.get_user_summary("2026-01-25")
```
**Response structure:**
```json
{
"totalSteps": 12543,
"totalKilocalories": 2456,
"activeKilocalories": 856,
"bmrKilocalories": 1600,
"intensityMinutesGoal": 150,
"vigorousIntensityMinutes": 35,
"moderateIntensityMinutes": 85
}
```
## Data Availability
### Required Device Features
| Metric | Required Hardware |
|--------|------------------|
| **Sleep stages** | Newer Garmin watches (2018+) |
| **Body Battery** | Firstbeat-enabled devices with HRV |
| **HRV** | Devices with optical HR sensor |
| **Stress** | Devices with all-day HR monitoring |
| **VO2 Max** | GPS + HR-enabled activities |
| **Respiration** | Newer devices (Fenix 6+, Venu 2+, etc.) |
### Historical Data
- Most metrics: Available for full account history
- HRV: May be limited on older devices
- Body Battery: Requires compatible device
## Rate Limits
Garmin enforces rate limits on their API:
**Observed limits:**
- ~50-100 requests per 10 minutes (varies)
- Excessive requests trigger temporary IP/account blocks
- Blocks typically last 15-60 minutes
**Best practices:**
- Cache data locally when possible
- Don't poll excessively (once per hour max for updates)
- Use date range queries instead of day-by-day loops when possible
- Add delays between bulk requests (1-2 seconds)
## Error Handling
### Common Errors
**Authentication Failed**
```
GarminConnectAuthenticationError
```
- Invalid credentials
- Account locked
- Two-factor authentication enabled (not supported)
**Connection Error**
```
GarminConnectConnectionError
```
- Network issues
- Garmin servers down
- Rate limit hit
**No Data**
- Returns `None` or empty list
- Device not worn
- Metric not supported on user's device
- Date has no data
## Library Documentation
**GitHub**: https://github.com/cyberjunky/python-garminconnect
**PyPI**: https://pypi.org/project/garminconnect/
## Alternative: Official Garmin Health API
For enterprise/commercial use:
- **Garmin Health API**: Requires partnership agreement
- **Use case**: App integration, health platforms
- **Not for**: Personal hobby projects
More info: https://www.garmin.com/en-US/health-enterprise/
## Comparison to Whoop API
| Feature | Garmin (unofficial) | Whoop (official) |
|---------|-------------------|------------------|
| **Authentication** | Email/password | OAuth 2.0 |
| **Stability** | May break anytime | Stable, versioned |
| **Rate limits** | Undocumented (~50/10min) | Documented (200/hr) |
| **Data access** | Full history | Full history |
| **Support** | Community library | Official support |
| **Terms of Service** | Gray area | Officially supported |
## Terms of Service Considerations
**Important**: Using the unofficial API may violate Garmin's Terms of Service. Use at your own risk:
- For **personal use** only
- Don't build commercial products on this
- Don't scrape excessively
- Consider official Garmin Health API for production use
## Tips for Reliability
1. **Cache aggressively**: Store data locally, only fetch new dates
2. **Error recovery**: Retry with exponential backoff on failures
3. **Monitor library updates**: Watch GitHub for breaking changes
4. **Have a fallback**: Manual CSV export from Garmin Connect web if API breaks
5. **Test before relying**: Verify data accuracy against Garmin Connect web UI
## Useful Resources
- [Garmin Connect Web](https://connect.garmin.com): Official interface
- [python-garminconnect GitHub](https://github.com/cyberjunky/python-garminconnect): Library source
- [Garmin Forums](https://forums.garmin.com): Community support
- [r/Garmin](https://reddit.com/r/Garmin): Reddit community
FILE:references/extended_capabilities.md
# Extended Garmin Capabilities
This skill now supports **comprehensive health tracking, time-based queries, and activity file analysis**.
## 🎯 Time-Based Queries
Ask questions like:
- "What was my heart rate at 3pm yesterday?"
- "What was my stress level at 10:30 this morning?"
- "What was my Body Battery at noon?"
### Usage
```bash
# Heart rate at specific time
python3 scripts/garmin_query.py heart_rate "3:00 PM" --date 2026-01-24
# Stress level
python3 scripts/garmin_query.py stress "14:30"
# Body Battery
python3 scripts/garmin_query.py body_battery "10:00 AM" --date 2026-01-23
# Steps at time
python3 scripts/garmin_query.py steps "17:00"
```
**Time formats supported:**
- `3:00 PM`, `3 PM` (12-hour)
- `15:00`, `15:30:45` (24-hour)
- `2026-01-24 15:30` (full datetime)
## 📊 Extended Metrics
### Training & Performance
```bash
# Training readiness (daily readiness score)
python3 scripts/garmin_data_extended.py training_readiness
# Training status (load, VO2 max trends)
python3 scripts/garmin_data_extended.py training_status
# Endurance score
python3 scripts/garmin_data_extended.py endurance_score
# Hill score
python3 scripts/garmin_data_extended.py hill_score
# Max metrics (VO2 max, etc.)
python3 scripts/garmin_data_extended.py max_metrics
# Fitness age
python3 scripts/garmin_data_extended.py fitness_age
```
### Body Composition & Health
```bash
# Body composition (weight, body fat %, muscle mass, BMI)
python3 scripts/garmin_data_extended.py body_composition --date 2026-01-24
# Weight history
python3 scripts/garmin_data_extended.py weigh_ins --start 2026-01-01 --end 2026-01-24
# Blood oxygen (SPO2)
python3 scripts/garmin_data_extended.py spo2 --date 2026-01-24
# Respiration (breathing rate throughout day)
python3 scripts/garmin_data_extended.py respiration
```
### Activity Metrics
```bash
# Detailed steps (time-series)
python3 scripts/garmin_data_extended.py steps --date 2026-01-24
# Floors climbed
python3 scripts/garmin_data_extended.py floors
# Intensity minutes (vigorous/moderate)
python3 scripts/garmin_data_extended.py intensity_minutes
# Hydration/water intake
python3 scripts/garmin_data_extended.py hydration
# Detailed stress (time-series throughout day)
python3 scripts/garmin_data_extended.py stress_detailed
# Intraday heart rate (all HR samples)
python3 scripts/garmin_data_extended.py hr_intraday
```
## 🗺️ Activity File Analysis (FIT/GPX)
Download and analyze activity files to answer questions like:
- "What was my elevation at mile 2?"
- "What was my pace when my heart rate was above 160?"
- "Show me my route on a map"
### Download Activity Files
```bash
# Download FIT file
python3 scripts/garmin_activity_files.py download --activity-id 12345678 --format fit
# Download GPX file (for GPS visualization)
python3 scripts/garmin_activity_files.py download --activity-id 12345678 --format gpx
# Download TCX file
python3 scripts/garmin_activity_files.py download --activity-id 12345678 --format tcx
```
### Parse Activity Files
```bash
# Parse FIT file (detailed metrics)
python3 scripts/garmin_activity_files.py parse --file /tmp/activity_12345678.fit
# Parse GPX file (GPS track)
python3 scripts/garmin_activity_files.py parse --file /tmp/activity_12345678.gpx
```
**FIT files contain:**
- GPS coordinates (lat/lon)
- Elevation/altitude
- Heart rate
- Cadence (steps/min for running, rpm for cycling)
- Power (watts, for cycling)
- Speed & pace
- Temperature
- Lap splits
### Query Activity Data
```bash
# What was my heart rate/elevation at 1500 meters into the run?
python3 scripts/garmin_activity_files.py query --file /tmp/activity_12345678.fit --distance 1500
# What was my data at a specific time during the activity?
python3 scripts/garmin_activity_files.py query --file /tmp/activity_12345678.fit --time "2026-01-24T10:15:30"
```
### Analyze Activity
```bash
# Get comprehensive statistics
python3 scripts/garmin_activity_files.py analyze --file /tmp/activity_12345678.fit
```
**Returns:**
- Average/max/min heart rate
- Elevation gain/max/min
- Average/max speed
- Average cadence
- Average/max power (cycling)
- Total distance
- Duration
## 🔍 Use Cases
### Health Monitoring
- "How has my VO2 max changed over the past month?"
- "What's my fitness age compared to my actual age?"
- "Show me my training load trend"
- "What was my SPO2 during sleep last night?"
### Activity Analysis
- "Map my running route from yesterday"
- "What was my heart rate at the steepest hill?"
- "Show me my pace per mile with elevation profile"
- "How does my cadence correlate with my heart rate?"
### Recovery Tracking
- "What's my training readiness today?"
- "When did my Body Battery start draining yesterday?"
- "How many intensity minutes did I get this week?"
### Time-Series Analysis
- "Graph my heart rate from 8am to 6pm"
- "Show me stress levels throughout the workday"
- "When was my Body Battery fully charged?"
## 📦 Dependencies
```bash
pip3 install garminconnect fitparse gpxpy
```
- **garminconnect**: Garmin Connect API wrapper
- **fitparse**: Parse FIT files (Garmin's binary format)
- **gpxpy**: Parse GPX files (GPS track format)
## 🛠️ Advanced Tips
### Get Activity ID
The activity ID is visible in the Garmin Connect URL:
```
https://connect.garmin.com/modern/activity/12345678
^^^^^^^^
```
Or find it programmatically:
```bash
python3 scripts/garmin_data.py activities --days 7
# Look for "activity_id" in each activity
```
### Batch Processing
```bash
# Get recent activity IDs
activities=$(python3 scripts/garmin_data.py activities --days 7 | jq -r '.activities[].activityId')
# Download all FIT files
for id in $activities; do
python3 scripts/garmin_activity_files.py download --activity-id $id --format fit
done
```
### Visualization Ideas
1. **Route Maps**: Use GPX files with Leaflet.js or Google Maps
2. **Elevation Profiles**: Plot elevation vs distance
3. **Heart Rate Zones**: Color-code route by HR zones
4. **Pace Heatmap**: Show where you were fastest/slowest
5. **Power/Cadence Correlation**: Cycling efficiency analysis
## 🚀 Future Enhancements
Potential additions:
- [ ] Automated route visualization
- [ ] Training plan tracking
- [ ] Workout recommendations
- [ ] Gear tracking & wear analysis
- [ ] Weather correlation with performance
- [ ] Social challenges & badges
- [ ] Menstrual cycle tracking
- [ ] Pregnancy tracking
- [ ] Race prediction accuracy tracking
FILE:references/health_analysis.md
# Health Data Analysis Guide - Garmin Edition
Science-backed framework for interpreting Garmin health metrics. Use this when the user asks about their health, trends, or wants insights from their data.
## HRV (Heart Rate Variability) - Nightly Average
### What it means
HRV measures the variation in time between heartbeats. It reflects autonomic nervous system (ANS) balance — specifically parasympathetic (rest-and-digest) activity. Higher HRV = more adaptable, resilient nervous system.
### Normal ranges (wrist-based wearable)
| Age | Low | Normal | High |
|-----|-----|--------|------|
| 20-29 | <25ms | 25-105ms | >105ms |
| 30-39 | <20ms | 20-80ms | >80ms |
| 40-49 | <15ms | 15-60ms | >60ms |
| 50-59 | <12ms | 12-45ms | >45ms |
| 60+ | <10ms | 10-35ms | >35ms |
*Note: Wrist-based HRV tends to read lower than chest strap. Individual baseline matters more than population norms.*
### Interpretation
- **Trend matters more than absolute value** — compare to personal weekly/monthly average
- **Declining HRV trend** (>10% below baseline for 3+ days): suggests accumulated stress, poor recovery, illness onset, or overtraining
- **Rising HRV trend**: improved fitness, better recovery, reduced stress
- **Acute drop**: poor sleep, alcohol, illness, intense training, emotional stress
- **Very low HRV** (<15ms consistently): consider medical consultation — linked to cardiovascular risk, chronic stress
### Garmin-specific notes
- Garmin calculates HRV during sleep (nightly average)
- Shows 7-day rolling average and baseline ranges (balanced low/high)
- **Status indicator**: Balanced, Unbalanced, Poor, Low
- Look for trends over weeks, not single days
### Key research findings
- Low HRV is associated with increased cardiovascular mortality (Harvard Health, PMC5624990)
- HRV reflects both physical AND psychological stress load
- 7-day rolling average provides more meaningful context than daily values
---
## Body Battery (0-100)
### What it means
Garmin's proprietary recovery metric based on:
- Heart rate variability
- Stress levels
- Sleep quality
- Activity intensity
Body Battery "charges" during rest/sleep and "drains" during activity/stress.
### Zones
| Level | Range | Meaning | Recommendation |
|-------|-------|---------|---------------|
| High | 75-100 | Fully charged, optimal energy | Ready for high-intensity training |
| Medium | 50-74 | Moderate energy | Good for regular workouts |
| Low | 25-49 | Limited energy reserves | Light activity, recovery focus |
| Very Low | 0-24 | Depleted | Prioritize rest and recovery |
### Patterns to watch
- **Peak daily value** (highest point after sleep): indicates overnight recovery quality
- **Charging rate** (how much you gain during sleep): poor charging = poor sleep/recovery
- **Draining rate** (how fast it depletes): rapid drain = high stress/activity
- **Consistently <50 peak**: chronic under-recovery — review sleep, stress, training load
- **Not fully charging** (peak <75 for 3+ days): accumulated fatigue
### Optimal pattern
- Charge to 75-100 overnight
- Maintain 50+ through most of the day
- Drop to 25-50 by evening
- Recharge during sleep
---
## Resting Heart Rate (RHR)
### Normal ranges
| Fitness level | RHR (bpm) |
|--------------|-----------|
| Athlete | 40-55 |
| Active adult | 55-65 |
| Average adult | 60-80 |
| Sedentary | 70-90 |
| Concerning | >90 |
### Interpretation
- **Trend matters**: RHR rising 3-5+ bpm above personal baseline for several days suggests accumulated fatigue, stress, illness, or dehydration
- **Acute spike**: alcohol, poor sleep, illness onset, overtraining
- **Decreasing RHR over weeks/months**: improving cardiovascular fitness
- **High RHR + Low HRV**: strong signal of poor recovery or health concern
### Garmin-specific
- Measured during sleep periods
- Tracks 7-day average and long-term trends
- Sudden increases often visible before illness symptoms
---
## Sleep Analysis
### Optimal distribution (for 7-8h total sleep)
| Stage | % of total | Ideal duration (8h) | Function |
|-------|-----------|---------------------|----------|
| Deep | 15-25% | 1.2-2.0h | Physical restoration, growth hormone, immune function, memory consolidation |
| REM | 20-25% | 1.6-2.0h | Emotional processing, learning, creativity, memory |
| Light | 50-60% | 4.0-4.8h | Transition stage, some memory processing |
| Awake | <10% | <0.8h | Normal brief awakenings |
### Sleep Score (0-100)
Garmin's composite score based on:
- Total sleep duration
- Sleep stage distribution
- Movement/restlessness
- Respiration quality
- Heart rate stability
| Score | Quality | Interpretation |
|-------|---------|---------------|
| 90-100 | Excellent | Optimal restorative sleep |
| 80-89 | Good | Quality sleep with minor issues |
| 60-79 | Fair | Adequate but could improve |
| 0-59 | Poor | Significant sleep deficiencies |
### Key thresholds
- **Total sleep needed** (adults): 7-9 hours (CDC recommendation)
- **Deep sleep <1h**: concerning — reduced physical recovery, weakened immunity
- **REM sleep <1.2h**: may affect emotional regulation, learning
- **Restless periods >15**: possible environment issues, sleep apnea, stress
- **Avg Sleep Score <70**: chronic sleep issues — review habits, environment, schedule
### Factors that reduce deep sleep
- Alcohol, caffeine (within 6h), late heavy meals, screen time, irregular schedule, aging
### Factors that increase deep sleep
- Exercise (not too close to bedtime), cool room temperature (60-67°F), consistent schedule, stress management
---
## Stress Levels
### What it means
Garmin calculates stress from HRV analysis throughout the day. Lower HRV = higher stress.
### Ranges
| Level | Score | Interpretation |
|-------|-------|---------------|
| Rest | 0-25 | Relaxed, recovering |
| Low | 26-50 | Calm, light activity |
| Medium | 51-75 | Moderate stress, normal activity |
| High | 76-100 | High stress, intense activity, or emotional pressure |
### Interpretation
- **All-day stress average**: Should be <50 for most days
- **High stress duration**: >4h per day may indicate chronic stress
- **Rest periods**: Should have some rest periods (0-25) throughout day
- **No rest periods**: concerning — may need stress management techniques
- **Stress spikes without activity**: emotional/mental stress
### Healthy pattern
- Morning rise (waking up)
- Activity-related spikes
- Several rest periods
- Evening decline
- Low stress during sleep
---
## Respiration Rate
### Normal ranges (breaths per minute during sleep)
| Range | Interpretation |
|-------|---------------|
| 12-16 | Normal, healthy |
| 10-12 | Normal for athletes |
| <10 or >20 | May indicate health concern |
### What to watch
- **Sudden increase**: illness, allergies, sleep apnea, stress
- **Consistently elevated**: chronic respiratory issues, poor fitness
- **High variability**: possible sleep-disordered breathing
---
## VO2 Max
### What it means
Maximum oxygen uptake during intense exercise (ml/kg/min). Gold standard for cardiorespiratory fitness.
### Ranges by age and gender (general population)
#### Men
| Age | Poor | Fair | Good | Excellent | Superior |
|-----|------|------|------|-----------|----------|
| 20-29 | <40 | 40-43 | 44-51 | 52-56 | >56 |
| 30-39 | <38 | 38-41 | 42-49 | 50-54 | >54 |
| 40-49 | <35 | 35-38 | 39-45 | 46-52 | >52 |
| 50-59 | <32 | 32-35 | 36-43 | 44-48 | >48 |
#### Women
| Age | Poor | Fair | Good | Excellent | Superior |
|-----|------|------|------|-----------|----------|
| 20-29 | <32 | 32-36 | 37-41 | 42-46 | >46 |
| 30-39 | <30 | 30-33 | 34-39 | 40-44 | >44 |
| 40-49 | <27 | 27-31 | 32-36 | 37-41 | >41 |
| 50-59 | <25 | 25-28 | 29-33 | 34-38 | >38 |
### Garmin-specific
- Estimated from running/walking activities with GPS + heart rate
- Updates after qualifying outdoor activities
- More accurate with chest strap heart rate
- Tracks trends over time
---
## Activity Intensity Minutes
### Weekly targets (WHO/AHA guidelines)
- **Moderate intensity**: 150 minutes/week
- **Vigorous intensity**: 75 minutes/week
- OR combination (2 min moderate = 1 min vigorous)
### Interpretation
- **Meeting target**: associated with reduced disease risk
- **Exceeding target**: additional health benefits up to ~300 min/week
- **Below target**: increased health risks
---
## Analysis Framework
When analyzing user data, follow this structure:
### 1. Quick Status (current state)
- Latest Body Battery level + peak value
- Last night's sleep score + hours
- Today's stress average
- Current resting heart rate
- Latest HRV reading + status
### 2. Trend Analysis (7-30 day window)
- HRV trend (rising/stable/declining vs baseline)
- Resting heart rate trend
- Sleep score average
- Body Battery charging pattern
- Activity intensity vs. recovery balance
### 3. Pattern Detection
- Day-of-week patterns (weekend effects, Monday dips)
- Sleep consistency
- Stress/recovery correlation
- Activity load vs. Body Battery recovery
- RHR elevation before illness
### 4. Actionable Insights (science-backed)
Based on data, suggest specific improvements:
**Sleep optimization:**
- Consistent bedtime/wake time (±30 min)
- 7-9 hours nightly
- Cool room (60-67°F)
- Dark, quiet environment
- No alcohol within 3h, no caffeine within 6h
**Recovery enhancement:**
- Match training intensity to Body Battery
- Build in rest days when BB not recovering above 75
- Stress management techniques when all-day stress >50
- Hydration monitoring
**Training periodization:**
- Push intensity on high Body Battery days (75+)
- Active recovery on medium days (50-74)
- Rest on low days (<50)
- Monitor HRV for overtraining signs
### 5. Flags / Alerts
- **Consistently low HRV** (<15ms) → suggest medical consultation
- **Elevated respiration** (>20 bpm during sleep) → possible breathing issue
- **RHR elevated 5+ bpm for 3+ days** → illness/overtraining watch
- **Sleep <6h average for 5+ days** → serious sleep debt, health risk
- **Body Battery not charging >75 for 5+ days** → chronic under-recovery
- **No rest stress periods** → chronic stress concern
---
## Prompt Template for Analysis
When asked "how am I doing?" or similar, fetch summary data and analyze:
```
Based on [USER]'s Garmin data for the last [N] days:
METRICS:
- Avg Body Battery Peak: X/100 (charging: good/poor)
- Avg HRV: Xms (vs baseline, trend: rising/falling/stable)
- HRV Status: Balanced/Unbalanced/Poor/Low
- Avg Resting HR: Xbpm (vs baseline: Xbpm)
- Avg Sleep: Xh, score: X/100
- Deep sleep avg: Xh (X% of total)
- REM avg: Xh (X% of total)
- Avg Stress: X (high stress duration: Xh/day)
- Activities: X workouts, X total calories
ANALYSIS:
[Apply the framework above — status, trends, patterns, insights, flags]
```
---
## Garmin vs Whoop: Key Differences
| Metric | Garmin | Whoop |
|--------|--------|-------|
| **Recovery** | Body Battery (0-100) | Recovery Score (0-100%) |
| **Strain** | Activity calories/intensity | Strain (0-21 scale) |
| **Stress** | All-day HRV-based stress | Not directly tracked |
| **Sleep scoring** | Composite score (0-100) | Performance % vs need |
| **Activities** | Extensive GPS/sport modes | Strain-focused tracking |
| **Primary use case** | Multi-sport fitness watch | Recovery-focused wearable |
---
## Important Disclaimers
- This is NOT medical advice — always recommend consulting a doctor for health concerns
- Wearable data has limitations in accuracy
- Individual baselines matter more than population norms
- One bad day doesn't indicate a problem — look for patterns over 3+ days
- Context matters: travel, altitude, menstrual cycle, medication, illness can all affect metrics
- Garmin estimates are based on algorithms and sensor data — not laboratory measurements
FILE:references/mcp_setup.md
# MCP Server for Standard Claude Desktop
> **This is a Clawdbot skill.** If you want to use Garmin data with standard Claude Desktop, use the dedicated MCP server instead.
## 📦 Dedicated MCP Server Repository
We've created a separate, purpose-built MCP server for Claude Desktop users:
### **[garmin-health-mcp-server](https://github.com/eversonl/garmin-health-mcp-server)**
This is a standalone Node.js-based MCP server optimized for Claude Desktop, with:
- ✅ Easy `npm install` setup
- ✅ Built-in authentication helper
- ✅ Test suite for verification
- ✅ Comprehensive documentation
- ✅ Full troubleshooting guide
## Quick Start (MCP Server)
```bash
# Clone the MCP server (separate repo)
git clone https://github.com/eversonl/garmin-health-mcp-server.git
cd garmin-health-mcp-server
# Install and setup
npm install
pip3 install garminconnect fitparse gpxpy
cp .env.example .env
# Edit .env with your credentials
# Authenticate
npm run auth
# Configure Claude Desktop (see full guide)
# Add to claude_desktop_config.json
```
**[📖 Full Installation Guide →](https://github.com/eversonl/garmin-health-mcp-server#readme)**
---
## This Skill (Clawdbot)
If you're using **Clawdbot**, you're in the right place! This skill provides:
- Automated health monitoring
- Scheduled reports and summaries
- Proactive workout notifications
- Integration with other Clawdbot skills
**[📖 Clawdbot Skill Setup →](../SKILL.md)**
---
## Using Both?
You can use both the Clawdbot skill and the MCP server simultaneously! They share authentication tokens, so you only need to log in once.
**Recommended setup:**
- **Clawdbot**: Morning health summaries, weekly reports, automated tracking
- **Claude Desktop**: Quick ad-hoc queries during the day
Authentication tokens are shared at `~/.clawdbot/garmin-tokens.json` (or `~/.garmin-tokens.json`).
FILE:scripts/debug_api.py
#!/usr/bin/env python3
import json
from datetime import datetime
from pathlib import Path
import sys
sys.path.insert(0, str(Path(__file__).parent))
from garmin_auth import get_client
client = get_client()
if not client:
print("NOT_AUTHED")
sys.exit(1)
# 先尝试获取设备
try:
devices = client.get_devices()
print("DEVICES:", json.dumps(devices, default=str)[:500])
except Exception as e:
print("DEVICES_ERROR:", str(e))
# 测试睡眠数据
try:
sleep = client.get_sleep_data("2026-04-04")
if sleep:
dto = sleep.get("dailySleepDTO", {})
print("SLEEP:", json.dumps(dto, default=str)[:300])
else:
print("SLEEP: None")
except Exception as e:
print("SLEEP_ERROR:", str(e))
# 测试心率
try:
hr = client.get_heart_rates("2026-04-04")
print("HR:", json.dumps(hr, default=str)[:300])
except Exception as e:
print("HR_ERROR:", str(e))
FILE:scripts/garmin_activity_files.py
#!/usr/bin/env python3
"""
Download and analyze Garmin activity FIT/GPX files.
Extract GPS, elevation, pace, heart rate, power, cadence, etc.
"""
import json
import sys
import os
from datetime import datetime
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent))
from garmin_auth import get_client
# Check for optional dependencies
try:
import fitparse
HAS_FITPARSE = True
except ImportError:
HAS_FITPARSE = False
try:
import gpxpy
import gpxpy.gpx
HAS_GPXPY = True
except ImportError:
HAS_GPXPY = False
def download_activity_file(client, activity_id, file_format="fit", output_dir="/tmp"):
"""Download activity FIT or GPX file."""
try:
output_path = f"{output_dir}/activity_{activity_id}.{file_format.lower()}"
if file_format.lower() == "fit":
data = client.download_activity(activity_id, dl_fmt=client.ActivityDownloadFormat.ORIGINAL)
elif file_format.lower() == "gpx":
data = client.download_activity(activity_id, dl_fmt=client.ActivityDownloadFormat.GPX)
elif file_format.lower() == "tcx":
data = client.download_activity(activity_id, dl_fmt=client.ActivityDownloadFormat.TCX)
else:
return {"error": f"Unsupported format: {file_format}"}
with open(output_path, 'wb') as f:
f.write(data)
return {"file": output_path, "activity_id": activity_id, "format": file_format}
except Exception as e:
return {"error": str(e), "activity_id": activity_id}
def parse_fit_file(file_path):
"""Parse FIT file and extract all data points."""
if not HAS_FITPARSE:
return {"error": "fitparse library not installed. Run: pip install fitparse"}
try:
fitfile = fitparse.FitFile(file_path)
# Extract different record types
records = []
laps = []
sessions = []
for record in fitfile.get_messages('record'):
data_point = {}
for field in record:
if field.value is not None:
data_point[field.name] = field.value
if data_point:
records.append(data_point)
for record in fitfile.get_messages('lap'):
lap_data = {}
for field in record:
if field.value is not None:
lap_data[field.name] = field.value
if lap_data:
laps.append(lap_data)
for record in fitfile.get_messages('session'):
session_data = {}
for field in record:
if field.value is not None:
session_data[field.name] = field.value
if session_data:
sessions.append(session_data)
return {
"records": records,
"laps": laps,
"sessions": sessions,
"total_records": len(records)
}
except Exception as e:
return {"error": str(e)}
def parse_gpx_file(file_path):
"""Parse GPX file and extract track points."""
if not HAS_GPXPY:
return {"error": "gpxpy library not installed. Run: pip install gpxpy"}
try:
with open(file_path, 'r') as f:
gpx = gpxpy.parse(f)
points = []
for track in gpx.tracks:
for segment in track.segments:
for point in segment.points:
points.append({
"latitude": point.latitude,
"longitude": point.longitude,
"elevation": point.elevation,
"time": point.time.isoformat() if point.time else None,
"speed": point.speed,
"hr": point.extensions.get("hr") if point.extensions else None
})
return {
"points": points,
"total_points": len(points),
"bounds": {
"min_lat": gpx.get_bounds().min_latitude,
"max_lat": gpx.get_bounds().max_latitude,
"min_lon": gpx.get_bounds().min_longitude,
"max_lon": gpx.get_bounds().max_longitude
} if gpx.get_bounds() else None
}
except Exception as e:
return {"error": str(e)}
def query_data_at_distance(data, distance_meters):
"""Find data point closest to a specific distance."""
if "records" in data:
records = data["records"]
elif "points" in data:
records = data["points"]
else:
return {"error": "No data records found"}
# Find closest by distance
closest = None
min_diff = float('inf')
for record in records:
if "distance" in record:
diff = abs(record["distance"] - distance_meters)
if diff < min_diff:
min_diff = diff
closest = record
return closest
def query_data_at_time(data, target_time):
"""Find data point at a specific time."""
if "records" in data:
records = data["records"]
elif "points" in data:
records = data["points"]
else:
return {"error": "No data records found"}
# Parse target time
if isinstance(target_time, str):
try:
target_dt = datetime.fromisoformat(target_time.replace("Z", "+00:00"))
except:
return {"error": f"Invalid time format: {target_time}"}
else:
target_dt = target_time
target_ts = target_dt.timestamp()
closest = None
min_diff = float('inf')
for record in records:
if "timestamp" in record:
if isinstance(record["timestamp"], datetime):
rec_ts = record["timestamp"].timestamp()
else:
continue
diff = abs(rec_ts - target_ts)
if diff < min_diff:
min_diff = diff
closest = record
return closest
def analyze_activity(data):
"""Analyze activity data and provide insights."""
if "error" in data:
return data
records = data.get("records", [])
if not records:
return {"error": "No data records to analyze"}
# Calculate statistics
hr_values = [r.get("heart_rate") for r in records if r.get("heart_rate")]
elevation_values = [r.get("altitude") or r.get("elevation") for r in records if r.get("altitude") or r.get("elevation")]
speed_values = [r.get("speed") for r in records if r.get("speed")]
cadence_values = [r.get("cadence") for r in records if r.get("cadence")]
power_values = [r.get("power") for r in records if r.get("power")]
analysis = {
"total_points": len(records),
"duration_seconds": None,
"distance_meters": None,
"heart_rate": {
"avg": sum(hr_values) / len(hr_values) if hr_values else None,
"max": max(hr_values) if hr_values else None,
"min": min(hr_values) if hr_values else None
},
"elevation": {
"max": max(elevation_values) if elevation_values else None,
"min": min(elevation_values) if elevation_values else None,
"gain": None # Would need to calculate from sequential points
},
"speed": {
"avg": sum(speed_values) / len(speed_values) if speed_values else None,
"max": max(speed_values) if speed_values else None
},
"cadence": {
"avg": sum(cadence_values) / len(cadence_values) if cadence_values else None
} if cadence_values else None,
"power": {
"avg": sum(power_values) / len(power_values) if power_values else None,
"max": max(power_values) if power_values else None
} if power_values else None
}
# Get duration and distance from first/last records
if records:
if "timestamp" in records[0] and "timestamp" in records[-1]:
if isinstance(records[0]["timestamp"], datetime):
duration = (records[-1]["timestamp"] - records[0]["timestamp"]).total_seconds()
analysis["duration_seconds"] = duration
if "distance" in records[-1]:
analysis["distance_meters"] = records[-1]["distance"]
return analysis
def main():
import argparse
parser = argparse.ArgumentParser(description="Analyze Garmin activity files")
parser.add_argument("action", choices=["download", "parse", "query", "analyze"],
help="Action to perform")
parser.add_argument("--activity-id", type=int, help="Activity ID")
parser.add_argument("--format", choices=["fit", "gpx", "tcx"], default="fit",
help="File format for download")
parser.add_argument("--file", help="Path to local FIT/GPX file")
parser.add_argument("--distance", type=float, help="Query data at distance (meters)")
parser.add_argument("--time", help="Query data at time (ISO format)")
parser.add_argument("--output-dir", default="/tmp", help="Output directory")
args = parser.parse_args()
if args.action == "download":
if not args.activity_id:
print('{"error": "activity-id required for download"}')
sys.exit(1)
client = get_client()
if not client:
print('{"error": "Not authenticated"}')
sys.exit(1)
result = download_activity_file(client, args.activity_id, args.format, args.output_dir)
print(json.dumps(result, indent=2))
elif args.action == "parse":
if not args.file:
print('{"error": "file path required for parse"}')
sys.exit(1)
if args.file.endswith('.fit'):
result = parse_fit_file(args.file)
elif args.file.endswith('.gpx'):
result = parse_gpx_file(args.file)
else:
result = {"error": "Unsupported file type. Use .fit or .gpx"}
print(json.dumps(result, indent=2, default=str))
elif args.action == "query":
if not args.file:
print('{"error": "file path required for query"}')
sys.exit(1)
# First parse the file
if args.file.endswith('.fit'):
data = parse_fit_file(args.file)
elif args.file.endswith('.gpx'):
data = parse_gpx_file(args.file)
else:
print('{"error": "Unsupported file type"}')
sys.exit(1)
if "error" in data:
print(json.dumps(data, indent=2))
sys.exit(1)
# Query
if args.distance is not None:
result = query_data_at_distance(data, args.distance)
elif args.time:
result = query_data_at_time(data, args.time)
else:
result = {"error": "Specify --distance or --time for query"}
print(json.dumps(result, indent=2, default=str))
elif args.action == "analyze":
if not args.file:
print('{"error": "file path required for analyze"}')
sys.exit(1)
# Parse and analyze
if args.file.endswith('.fit'):
data = parse_fit_file(args.file)
elif args.file.endswith('.gpx'):
data = parse_gpx_file(args.file)
else:
print('{"error": "Unsupported file type"}')
sys.exit(1)
result = analyze_activity(data)
print(json.dumps(result, indent=2, default=str))
if __name__ == "__main__":
main()
FILE:scripts/garmin_auth.py
#!/usr/bin/env python3
"""
Garmin Connect authentication helper.
Handles login and stores session tokens.
"""
import json
import os
import sys
from pathlib import Path
from datetime import datetime
import argparse
try:
from garminconnect import Garmin, GarminConnectAuthenticationError, GarminConnectConnectionError
except ImportError:
print("❌ garminconnect library not installed", file=sys.stderr)
print("Install with: pip3 install garminconnect", file=sys.stderr)
sys.exit(1)
TOKEN_DIR = Path.home() / ".clawdbot" / "garmin"
CONFIG_FILE = Path(__file__).parent.parent / "config.json"
def load_config():
"""Load credentials from config file."""
if not CONFIG_FILE.exists():
return None
try:
with open(CONFIG_FILE) as f:
return json.load(f)
except Exception as e:
print(f"⚠️ Failed to load config: {e}", file=sys.stderr)
return None
def login(email, password):
"""Perform login and save tokens using garminconnect's tokenstore."""
try:
print(f"🔐 Logging in as {email}...", file=sys.stderr)
# Create token directory
TOKEN_DIR.mkdir(parents=True, exist_ok=True)
tokenstore = str(TOKEN_DIR)
# Create client and login (don't pass tokenstore on first login)
client = Garmin(email, password)
client.login() # Initial login without tokenstore
# Save tokens to tokenstore
client.garth.dump(tokenstore)
print(f"✅ Tokens saved to {tokenstore}", file=sys.stderr)
# Test the connection
try:
profile = client.get_user_summary(datetime.now().strftime("%Y-%m-%d"))
print(f"✅ Login successful! User: {profile.get('displayName', 'Unknown')}", file=sys.stderr)
except Exception as e:
print(f"✅ Login successful! (Unable to fetch profile: {e})", file=sys.stderr)
# Make tokenstore directory secure
TOKEN_DIR.chmod(0o700)
return True
except GarminConnectAuthenticationError as e:
print(f"❌ Authentication failed: {e}", file=sys.stderr)
print("Check your email/password and try again.", file=sys.stderr)
return False
except Exception as e:
print(f"❌ Login error: {e}", file=sys.stderr)
return False
def get_client():
"""Get authenticated Garmin client, using saved tokens if available."""
tokenstore = str(TOKEN_DIR)
if not TOKEN_DIR.exists():
return None
try:
# Try to use saved tokens
client = Garmin()
client.login(tokenstore=tokenstore)
# Test if tokens still work
client.get_user_summary(datetime.now().strftime("%Y-%m-%d"))
return client
except Exception as e:
print(f"⚠️ Saved tokens expired or invalid: {e}", file=sys.stderr)
return None
def check_status():
"""Check if we have valid authentication."""
tokenstore = str(TOKEN_DIR)
if not TOKEN_DIR.exists():
print("❌ Not authenticated", file=sys.stderr)
print("Run: python3 scripts/garmin_auth.py login", file=sys.stderr)
return False
print(f"✅ Token store found at {tokenstore}", file=sys.stderr)
# Test if they work
client = get_client()
if client:
try:
profile = client.get_user_summary(datetime.now().strftime("%Y-%m-%d"))
print(f"✅ Authentication valid! User: {profile.get('displayName', 'Unknown')}", file=sys.stderr)
return True
except Exception as e:
print(f"⚠️ Tokens may be expired: {e}", file=sys.stderr)
return False
print("❌ Authentication invalid. Please login again.", file=sys.stderr)
return False
def main():
parser = argparse.ArgumentParser(description="Garmin Connect authentication")
subparsers = parser.add_subparsers(dest="command", help="Command")
# Login command
login_parser = subparsers.add_parser("login", help="Login to Garmin Connect")
login_parser.add_argument("--email", help="Garmin account email (or set via env/config)")
login_parser.add_argument("--password", help="Garmin account password (or set via env/config)")
# Status command
subparsers.add_parser("status", help="Check authentication status")
args = parser.parse_args()
if args.command == "login":
email = args.email
password = args.password
# Priority: CLI args > config.json > environment variables
if not email or not password:
config = load_config()
if config:
email = email or config.get("email")
password = password or config.get("password")
if not email or not password:
email = email or os.getenv("GARMIN_EMAIL")
password = password or os.getenv("GARMIN_PASSWORD")
if not email or not password:
print("❌ Email and password required", file=sys.stderr)
print("Set via:", file=sys.stderr)
print(" 1. CLI: --email and --password", file=sys.stderr)
print(" 2. Config: create config.json from config.example.json", file=sys.stderr)
print(" 3. Env vars: GARMIN_EMAIL and GARMIN_PASSWORD", file=sys.stderr)
print(" 4. Clawdbot config: skills.entries.garmin-health-analysis.env", file=sys.stderr)
sys.exit(1)
success = login(email, password)
sys.exit(0 if success else 1)
elif args.command == "status":
success = check_status()
sys.exit(0 if success else 1)
else:
parser.print_help()
sys.exit(1)
if __name__ == "__main__":
main()
FILE:scripts/garmin_chart.py
#!/usr/bin/env python3
"""
Generate interactive HTML charts from Garmin health data.
Uses Chart.js for visualizations.
"""
import json
import sys
import argparse
import webbrowser
from pathlib import Path
from datetime import datetime
# Import auth and data helpers
sys.path.insert(0, str(Path(__file__).parent))
from garmin_auth import get_client
from garmin_data import fetch_sleep, fetch_hrv, fetch_body_battery, fetch_heart_rate, fetch_activities, fetch_stress
def generate_html(charts_data, title="Garmin Health Dashboard"):
"""Generate HTML with Chart.js visualizations."""
html = f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{title}</title>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js"></script>
<style>
* {{
margin: 0;
padding: 0;
box-sizing: border-box;
}}
body {{
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
color: #fff;
padding: 20px;
min-height: 100vh;
}}
.container {{
max-width: 1400px;
margin: 0 auto;
}}
h1 {{
text-align: center;
margin-bottom: 30px;
font-size: 2.5rem;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
}}
.stats-grid {{
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 40px;
}}
.stat-card {{
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-radius: 12px;
padding: 20px;
text-align: center;
border: 1px solid rgba(255, 255, 255, 0.2);
}}
.stat-value {{
font-size: 2.5rem;
font-weight: bold;
margin: 10px 0;
}}
.stat-label {{
font-size: 0.9rem;
opacity: 0.8;
text-transform: uppercase;
letter-spacing: 1px;
}}
.charts-grid {{
display: grid;
grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));
gap: 30px;
}}
.chart-container {{
background: rgba(255, 255, 255, 0.05);
backdrop-filter: blur(10px);
border-radius: 16px;
padding: 30px;
border: 1px solid rgba(255, 255, 255, 0.1);
}}
.chart-title {{
font-size: 1.3rem;
margin-bottom: 20px;
text-align: center;
font-weight: 600;
}}
canvas {{
max-height: 400px;
}}
.footer {{
text-align: center;
margin-top: 40px;
opacity: 0.6;
font-size: 0.9rem;
}}
</style>
</head>
<body>
<div class="container">
<h1>{title}</h1>
<div class="stats-grid" id="stats"></div>
<div class="charts-grid" id="charts"></div>
<div class="footer">
Generated on {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
</div>
</div>
<script>
Chart.defaults.color = '#fff';
Chart.defaults.borderColor = 'rgba(255, 255, 255, 0.1)';
const chartsData = {json.dumps(charts_data)};
// Render stats cards
function renderStats(stats) {{
const container = document.getElementById('stats');
for (const [label, value] of Object.entries(stats)) {{
const card = document.createElement('div');
card.className = 'stat-card';
card.innerHTML = `
<div class="stat-label">{label}</div>
<div class="stat-value">{value}</div>
`;
container.appendChild(card);
}}
}}
// Render chart
function renderChart(config) {{
const container = document.createElement('div');
container.className = 'chart-container';
const title = document.createElement('div');
title.className = 'chart-title';
title.textContent = config.title;
container.appendChild(title);
const canvas = document.createElement('canvas');
container.appendChild(canvas);
document.getElementById('charts').appendChild(container);
new Chart(canvas, config.chart);
}}
// Render all data
if (chartsData.stats) {{
renderStats(chartsData.stats);
}}
if (chartsData.charts) {{
chartsData.charts.forEach(renderChart);
}}
</script>
</body>
</html>"""
return html
def create_sleep_chart(sleep_data):
"""Create sleep analysis chart."""
dates = [s["date"] for s in sleep_data if s.get("sleep_time_seconds")]
hours = [s["sleep_time_seconds"] / 3600 for s in sleep_data if s.get("sleep_time_seconds")]
scores = [s.get("sleep_score", 0) for s in sleep_data if s.get("sleep_score")]
avg_hours = sum(hours) / len(hours) if hours else 0
avg_score = sum(scores) / len(scores) if scores else 0
return {
"stats": {
"Avg Sleep": f"{avg_hours:.1f}h",
"Avg Score": f"{avg_score:.0f}/100"
},
"chart": {
"title": "Sleep Analysis",
"chart": {
"type": "bar",
"data": {
"labels": dates,
"datasets": [
{
"label": "Sleep Hours",
"data": hours,
"backgroundColor": "rgba(54, 162, 235, 0.6)",
"borderColor": "rgba(54, 162, 235, 1)",
"borderWidth": 2,
"yAxisID": "y"
},
{
"label": "Sleep Score",
"data": scores,
"type": "line",
"borderColor": "rgba(255, 206, 86, 1)",
"backgroundColor": "rgba(255, 206, 86, 0.1)",
"borderWidth": 3,
"tension": 0.4,
"yAxisID": "y1"
}
]
},
"options": {
"responsive": True,
"interaction": {
"mode": "index",
"intersect": False
},
"scales": {
"y": {
"type": "linear",
"display": True,
"position": "left",
"title": {
"display": True,
"text": "Hours"
}
},
"y1": {
"type": "linear",
"display": True,
"position": "right",
"title": {
"display": True,
"text": "Score"
},
"grid": {
"drawOnChartArea": False
},
"max": 100
}
},
"plugins": {
"legend": {
"display": True
}
}
}
}
}
}
def create_body_battery_chart(bb_data):
"""Create Body Battery (recovery) chart."""
dates = [b["date"] for b in bb_data if b.get("charged") is not None]
charged = [b.get("charged", 0) for b in bb_data if b.get("charged") is not None]
highest = [b.get("highest", 0) for b in bb_data if b.get("highest") is not None]
avg_charged = sum(charged) / len(charged) if charged else 0
avg_highest = sum(highest) / len(highest) if highest else 0
# Color code based on levels
colors = []
for val in highest:
if val >= 75:
colors.append("rgba(75, 192, 192, 0.6)") # Green
elif val >= 50:
colors.append("rgba(255, 206, 86, 0.6)") # Yellow
elif val >= 25:
colors.append("rgba(255, 159, 64, 0.6)") # Orange
else:
colors.append("rgba(255, 99, 132, 0.6)") # Red
return {
"stats": {
"Avg Charged": f"+{avg_charged:.0f}",
"Avg Peak": f"{avg_highest:.0f}/100"
},
"chart": {
"title": "Body Battery (Recovery)",
"chart": {
"type": "bar",
"data": {
"labels": dates,
"datasets": [{
"label": "Highest Body Battery",
"data": highest,
"backgroundColor": colors,
"borderWidth": 0
}]
},
"options": {
"responsive": True,
"scales": {
"y": {
"beginAtZero": True,
"max": 100,
"title": {
"display": True,
"text": "Body Battery"
}
}
},
"plugins": {
"legend": {
"display": False
}
}
}
}
}
}
def create_hrv_chart(hrv_data, hr_data):
"""Create HRV and resting heart rate trend chart."""
dates = [h["date"] for h in hrv_data if h.get("last_night_avg")]
hrv_values = [h.get("last_night_avg", 0) for h in hrv_data if h.get("last_night_avg")]
# Match dates with heart rate data
hr_map = {h["date"]: h.get("resting_hr") for h in hr_data if h.get("resting_hr")}
rhr_values = [hr_map.get(date, 0) for date in dates]
avg_hrv = sum(hrv_values) / len(hrv_values) if hrv_values else 0
avg_rhr = sum(rhr_values) / len(rhr_values) if rhr_values else 0
return {
"stats": {
"Avg HRV": f"{avg_hrv:.0f} ms",
"Avg RHR": f"{avg_rhr:.0f} bpm"
},
"chart": {
"title": "HRV & Resting Heart Rate",
"chart": {
"type": "line",
"data": {
"labels": dates,
"datasets": [
{
"label": "HRV (ms)",
"data": hrv_values,
"borderColor": "rgba(153, 102, 255, 1)",
"backgroundColor": "rgba(153, 102, 255, 0.1)",
"borderWidth": 3,
"tension": 0.4,
"yAxisID": "y"
},
{
"label": "Resting HR (bpm)",
"data": rhr_values,
"borderColor": "rgba(255, 99, 132, 1)",
"backgroundColor": "rgba(255, 99, 132, 0.1)",
"borderWidth": 3,
"tension": 0.4,
"yAxisID": "y1"
}
]
},
"options": {
"responsive": True,
"interaction": {
"mode": "index",
"intersect": False
},
"scales": {
"y": {
"type": "linear",
"display": True,
"position": "left",
"title": {
"display": True,
"text": "HRV (ms)"
}
},
"y1": {
"type": "linear",
"display": True,
"position": "right",
"title": {
"display": True,
"text": "Heart Rate (bpm)"
},
"grid": {
"drawOnChartArea": False
}
}
},
"plugins": {
"legend": {
"display": True
}
}
}
}
}
}
def create_activities_chart(activities_data):
"""Create activities/workouts summary chart."""
# Group by type
types = {}
for activity in activities_data:
activity_type = activity.get("activity_type", "Unknown")
if activity_type not in types:
types[activity_type] = {"count": 0, "calories": 0, "duration": 0}
types[activity_type]["count"] += 1
types[activity_type]["calories"] += activity.get("calories", 0)
types[activity_type]["duration"] += activity.get("duration_seconds", 0) / 3600
labels = list(types.keys())
counts = [types[t]["count"] for t in labels]
calories = [types[t]["calories"] for t in labels]
total_activities = sum(counts)
total_calories = sum(calories)
return {
"stats": {
"Activities": f"{total_activities}",
"Total Calories": f"{total_calories:.0f}"
},
"chart": {
"title": "Activities Summary",
"chart": {
"type": "bar",
"data": {
"labels": labels,
"datasets": [
{
"label": "Count",
"data": counts,
"backgroundColor": "rgba(75, 192, 192, 0.6)",
"borderColor": "rgba(75, 192, 192, 1)",
"borderWidth": 2,
"yAxisID": "y"
},
{
"label": "Calories",
"data": calories,
"backgroundColor": "rgba(255, 159, 64, 0.6)",
"borderColor": "rgba(255, 159, 64, 1)",
"borderWidth": 2,
"yAxisID": "y1"
}
]
},
"options": {
"responsive": True,
"scales": {
"y": {
"type": "linear",
"display": True,
"position": "left",
"title": {
"display": True,
"text": "Count"
}
},
"y1": {
"type": "linear",
"display": True,
"position": "right",
"title": {
"display": True,
"text": "Calories"
},
"grid": {
"drawOnChartArea": False
}
}
},
"plugins": {
"legend": {
"display": True
}
}
}
}
}
}
def main():
parser = argparse.ArgumentParser(description="Generate Garmin health charts")
parser.add_argument("chart", choices=["sleep", "body_battery", "hrv", "activities", "dashboard"],
help="Type of chart to generate")
parser.add_argument("--days", type=int, default=30, help="Number of days (default: 30)")
parser.add_argument("--output", help="Output HTML file (default: opens in browser)")
args = parser.parse_args()
# Get authenticated client
client = get_client()
if not client:
print("❌ Not authenticated. Run: python3 scripts/garmin_auth.py login", file=sys.stderr)
sys.exit(1)
print(f"📊 Fetching {args.days} days of data...", file=sys.stderr)
# Prepare chart data
charts_data = {"stats": {}, "charts": []}
if args.chart in ["sleep", "dashboard"]:
print(" - Sleep data", file=sys.stderr)
sleep_data = fetch_sleep(client, args.days).get("sleep", [])
if sleep_data:
result = create_sleep_chart(sleep_data)
charts_data["stats"].update(result.get("stats", {}))
charts_data["charts"].append(result["chart"])
if args.chart in ["body_battery", "dashboard"]:
print(" - Body Battery data", file=sys.stderr)
bb_data = fetch_body_battery(client, args.days).get("body_battery", [])
if bb_data:
result = create_body_battery_chart(bb_data)
charts_data["stats"].update(result.get("stats", {}))
charts_data["charts"].append(result["chart"])
if args.chart in ["hrv", "dashboard"]:
print(" - HRV & Heart Rate data", file=sys.stderr)
hrv_data = fetch_hrv(client, args.days).get("hrv", [])
hr_data = fetch_heart_rate(client, args.days).get("heart_rate", [])
if hrv_data:
result = create_hrv_chart(hrv_data, hr_data)
charts_data["stats"].update(result.get("stats", {}))
charts_data["charts"].append(result["chart"])
if args.chart in ["activities", "dashboard"]:
print(" - Activities data", file=sys.stderr)
activities_data = fetch_activities(client, args.days).get("activities", [])
if activities_data:
result = create_activities_chart(activities_data)
charts_data["stats"].update(result.get("stats", {}))
charts_data["charts"].append(result["chart"])
# Generate HTML
title = f"Garmin {args.chart.title()} - Last {args.days} Days"
html = generate_html(charts_data, title)
# Save or open
if args.output:
output_path = Path(args.output).expanduser()
output_path.write_text(html)
print(f"✅ Chart saved to {output_path}", file=sys.stderr)
else:
# Save to temp file and open
import tempfile
with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False) as f:
f.write(html)
temp_path = f.name
print(f"✅ Opening chart in browser...", file=sys.stderr)
webbrowser.open(f"file://{temp_path}")
if __name__ == "__main__":
main()
FILE:scripts/garmin_data.py
#!/usr/bin/env python3
"""
Fetch health data from Garmin Connect.
Outputs JSON to stdout for parsing by the agent.
"""
import json
import sys
import argparse
from datetime import datetime, timedelta
from pathlib import Path
# Import auth helper
sys.path.insert(0, str(Path(__file__).parent))
from garmin_auth import get_client
try:
from garminconnect import Garmin
except ImportError:
print('{"error": "garminconnect not installed. Run: pip3 install garminconnect"}', file=sys.stderr)
sys.exit(1)
def get_date_range(days=None, start=None, end=None):
"""Calculate date range for queries."""
if start and end:
return start, end
end_date = datetime.now()
start_date = end_date - timedelta(days=days or 7)
return start_date.strftime("%Y-%m-%d"), end_date.strftime("%Y-%m-%d")
def fetch_sleep(client, days=7, start=None, end=None):
"""Fetch sleep data."""
start_date, end_date = get_date_range(days, start, end)
try:
sleep_data = []
current = datetime.strptime(start_date, "%Y-%m-%d")
end = datetime.strptime(end_date, "%Y-%m-%d")
while current <= end:
date_str = current.strftime("%Y-%m-%d")
try:
data = client.get_sleep_data(date_str)
if data:
# Sleep data is nested inside dailySleepDTO
sleep_dto = data.get("dailySleepDTO", {})
if sleep_dto:
sleep_data.append({
"date": date_str,
"sleep_time_seconds": sleep_dto.get("sleepTimeSeconds"),
"deep_sleep_seconds": sleep_dto.get("deepSleepSeconds"),
"light_sleep_seconds": sleep_dto.get("lightSleepSeconds"),
"rem_sleep_seconds": sleep_dto.get("remSleepSeconds"),
"awake_seconds": sleep_dto.get("awakeSleepSeconds"),
"sleep_score": sleep_dto.get("sleepScores", {}).get("overall", {}).get("value"),
"restless_periods": data.get("restlessMomentsCount"), # This is on root
"avg_hr": sleep_dto.get("averageHeartRate"),
"avg_hrv": data.get("avgOvernightHrv"), # This is on root
"avg_respiration": sleep_dto.get("averageRespirationValue")
})
except Exception as e:
print(f"⚠️ No sleep data for {date_str}: {e}", file=sys.stderr)
current += timedelta(days=1)
return {"sleep": sleep_data, "start": start_date, "end": end_date}
except Exception as e:
return {"error": str(e)}
def fetch_hrv(client, days=7, start=None, end=None):
"""Fetch HRV data."""
start_date, end_date = get_date_range(days, start, end)
try:
hrv_data = []
current = datetime.strptime(start_date, "%Y-%m-%d")
end = datetime.strptime(end_date, "%Y-%m-%d")
while current <= end:
date_str = current.strftime("%Y-%m-%d")
try:
data = client.get_hrv_data(date_str)
if data and "hrvSummary" in data:
summary = data["hrvSummary"]
hrv_data.append({
"date": date_str,
"last_night_avg": summary.get("lastNightAvg"),
"last_night_5min_high": summary.get("lastNight5MinHigh"),
"last_night_5min_low": summary.get("lastNight5MinLow"),
"weekly_avg": summary.get("weeklyAvg"),
"baseline_balanced_low": summary.get("baselineBalancedLow"),
"baseline_balanced_high": summary.get("baselineBalancedHigh"),
"status": summary.get("status")
})
except Exception:
pass
current += timedelta(days=1)
return {"hrv": hrv_data, "start": start_date, "end": end_date}
except Exception as e:
return {"error": str(e)}
def fetch_body_battery(client, days=7, start=None, end=None):
"""Fetch Body Battery data (Garmin's recovery metric)."""
start_date, end_date = get_date_range(days, start, end)
try:
bb_data = []
current = datetime.strptime(start_date, "%Y-%m-%d")
end = datetime.strptime(end_date, "%Y-%m-%d")
while current <= end:
date_str = current.strftime("%Y-%m-%d")
try:
data = client.get_body_battery(date_str)
if data and len(data) > 0:
day_data = data[0]
charged = day_data.get("charged")
drained = day_data.get("drained")
# Parse bodyBatteryValuesArray to get highest/lowest
values_array = day_data.get("bodyBatteryValuesArray", [])
values = [v[1] for v in values_array if len(v) > 1] # Extract values from [timestamp, value] pairs
highest = max(values) if values else None
lowest = min(values) if values else None
bb_data.append({
"date": date_str,
"charged": charged,
"drained": drained,
"highest": highest,
"lowest": lowest
})
except Exception:
pass
current += timedelta(days=1)
return {"body_battery": bb_data, "start": start_date, "end": end_date}
except Exception as e:
return {"error": str(e)}
def fetch_heart_rate(client, days=7, start=None, end=None):
"""Fetch heart rate data."""
start_date, end_date = get_date_range(days, start, end)
try:
hr_data = []
current = datetime.strptime(start_date, "%Y-%m-%d")
end = datetime.strptime(end_date, "%Y-%m-%d")
while current <= end:
date_str = current.strftime("%Y-%m-%d")
try:
data = client.get_heart_rates(date_str)
if data:
hr_data.append({
"date": date_str,
"resting_hr": data.get("restingHeartRate"),
"max_hr": data.get("maxHeartRate"),
"min_hr": data.get("minHeartRate")
})
except Exception:
pass
current += timedelta(days=1)
return {"heart_rate": hr_data, "start": start_date, "end": end_date}
except Exception as e:
return {"error": str(e)}
def fetch_activities(client, days=7, start=None, end=None):
"""Fetch activities/workouts."""
start_date, end_date = get_date_range(days, start, end)
try:
# Garmin API uses offset-based pagination
activities = client.get_activities_by_date(start_date, end_date, activitytype="")
activity_list = []
for activity in activities:
activity_list.append({
"date": activity.get("startTimeLocal", "").split(" ")[0],
"activity_type": activity.get("activityType", {}).get("typeKey"),
"activity_name": activity.get("activityName"),
"duration_seconds": activity.get("duration"),
"distance_meters": activity.get("distance"),
"calories": activity.get("calories"),
"avg_hr": activity.get("averageHR"),
"max_hr": activity.get("maxHR"),
"elevation_gain": activity.get("elevationGain"),
"avg_speed": activity.get("averageSpeed")
})
return {"activities": activity_list, "start": start_date, "end": end_date, "count": len(activity_list)}
except Exception as e:
return {"error": str(e)}
def fetch_stress(client, days=7, start=None, end=None):
"""Fetch stress levels."""
start_date, end_date = get_date_range(days, start, end)
try:
stress_data = []
current = datetime.strptime(start_date, "%Y-%m-%d")
end = datetime.strptime(end_date, "%Y-%m-%d")
while current <= end:
date_str = current.strftime("%Y-%m-%d")
try:
data = client.get_stress_data(date_str)
if data:
stress_data.append({
"date": date_str,
"avg_stress": data.get("avgStressLevel"),
"max_stress": data.get("maxStressLevel"),
"rest_stress": data.get("restStressLevel"),
"activity_stress": data.get("activityStressLevel"),
"low_stress_duration": data.get("lowStressDuration"),
"medium_stress_duration": data.get("mediumStressDuration"),
"high_stress_duration": data.get("highStressDuration")
})
except Exception:
pass
current += timedelta(days=1)
return {"stress": stress_data, "start": start_date, "end": end_date}
except Exception as e:
return {"error": str(e)}
def fetch_summary(client, days=7, start=None, end=None):
"""Fetch combined summary with key metrics."""
start_date, end_date = get_date_range(days, start, end)
try:
# Fetch multiple data types
sleep = fetch_sleep(client, days, start, end).get("sleep", [])
hrv = fetch_hrv(client, days, start, end).get("hrv", [])
bb = fetch_body_battery(client, days, start, end).get("body_battery", [])
hr = fetch_heart_rate(client, days, start, end).get("heart_rate", [])
activities = fetch_activities(client, days, start, end).get("activities", [])
# Calculate averages (handle None values)
sleep_times = [s.get("sleep_time_seconds") for s in sleep if s.get("sleep_time_seconds")]
avg_sleep_hours = (sum(sleep_times) / len(sleep_times) / 3600) if sleep_times else 0
sleep_scores = [s.get("sleep_score") for s in sleep if s.get("sleep_score") is not None]
avg_sleep_score = (sum(sleep_scores) / len(sleep_scores)) if sleep_scores else 0
hrv_values = [h.get("last_night_avg") for h in hrv if h.get("last_night_avg") is not None]
avg_hrv = (sum(hrv_values) / len(hrv_values)) if hrv_values else 0
rhr_values = [h.get("resting_hr") for h in hr if h.get("resting_hr") is not None]
avg_rhr = (sum(rhr_values) / len(rhr_values)) if rhr_values else 0
bb_charged_values = [b.get("charged") for b in bb if b.get("charged") is not None]
avg_bb_charged = (sum(bb_charged_values) / len(bb_charged_values)) if bb_charged_values else 0
return {
"summary": {
"period": f"{start_date} to {end_date}",
"days": days,
"avg_sleep_hours": round(avg_sleep_hours, 1),
"avg_sleep_score": round(avg_sleep_score, 1),
"avg_hrv_ms": round(avg_hrv, 1),
"avg_resting_hr": round(avg_rhr, 1),
"avg_body_battery_charged": round(avg_bb_charged, 1),
"total_activities": len(activities),
"total_calories": sum(a.get("calories", 0) for a in activities if a.get("calories"))
},
"sleep": sleep,
"hrv": hrv,
"body_battery": bb,
"heart_rate": hr,
"activities": activities
}
except Exception as e:
return {"error": str(e)}
def fetch_profile(client):
"""Fetch user profile."""
try:
profile = client.get_full_name()
stats = client.get_user_summary(datetime.now().strftime("%Y-%m-%d"))
return {
"profile": {
"name": profile,
"display_name": stats.get("displayName"),
"email": stats.get("email")
}
}
except Exception as e:
return {"error": str(e)}
def main():
parser = argparse.ArgumentParser(description="Fetch Garmin health data")
parser.add_argument("metric", choices=["sleep", "hrv", "body_battery", "heart_rate", "activities", "stress", "summary", "profile"],
help="Type of data to fetch")
parser.add_argument("--days", type=int, default=7, help="Number of days to fetch (default: 7)")
parser.add_argument("--start", help="Start date (YYYY-MM-DD)")
parser.add_argument("--end", help="End date (YYYY-MM-DD)")
args = parser.parse_args()
# Get authenticated client
client = get_client()
if not client:
print('{"error": "Not authenticated. Run: python3 scripts/garmin_auth.py login --email YOUR_EMAIL --password YOUR_PASSWORD"}')
sys.exit(1)
# Fetch requested data
if args.metric == "sleep":
result = fetch_sleep(client, args.days, args.start, args.end)
elif args.metric == "hrv":
result = fetch_hrv(client, args.days, args.start, args.end)
elif args.metric == "body_battery":
result = fetch_body_battery(client, args.days, args.start, args.end)
elif args.metric == "heart_rate":
result = fetch_heart_rate(client, args.days, args.start, args.end)
elif args.metric == "activities":
result = fetch_activities(client, args.days, args.start, args.end)
elif args.metric == "stress":
result = fetch_stress(client, args.days, args.start, args.end)
elif args.metric == "summary":
result = fetch_summary(client, args.days, args.start, args.end)
elif args.metric == "profile":
result = fetch_profile(client)
# Output JSON
print(json.dumps(result, indent=2))
if __name__ == "__main__":
main()
FILE:scripts/garmin_data_extended.py
#!/usr/bin/env python3
"""
Extended Garmin data fetching - training metrics, body composition, time-series data.
"""
import json
import sys
from datetime import datetime, timedelta
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent))
from garmin_auth import get_client
def fetch_training_readiness(client, date=None):
"""Fetch daily training readiness score."""
if not date:
date = datetime.now().strftime("%Y-%m-%d")
try:
data = client.get_training_readiness(date)
return {"training_readiness": data, "date": date}
except Exception as e:
return {"error": str(e), "date": date}
def fetch_training_status(client, date=None):
"""Fetch training status (load, VO2 max, etc.)."""
if not date:
date = datetime.now().strftime("%Y-%m-%d")
try:
data = client.get_training_status(date)
return {"training_status": data, "date": date}
except Exception as e:
return {"error": str(e), "date": date}
def fetch_body_composition(client, date=None):
"""Fetch body composition (weight, body fat %, muscle mass, etc.)."""
if not date:
date = datetime.now().strftime("%Y-%m-%d")
try:
data = client.get_body_composition(date)
return {"body_composition": data, "date": date}
except Exception as e:
return {"error": str(e), "date": date}
def fetch_weigh_ins(client, start_date=None, end_date=None):
"""Fetch weight measurements over time."""
if not start_date:
start_date = (datetime.now() - timedelta(days=30)).strftime("%Y-%m-%d")
if not end_date:
end_date = datetime.now().strftime("%Y-%m-%d")
try:
data = client.get_weigh_ins(start_date, end_date)
return {"weigh_ins": data, "start": start_date, "end": end_date}
except Exception as e:
return {"error": str(e)}
def fetch_spo2(client, date=None):
"""Fetch blood oxygen (SPO2) data."""
if not date:
date = datetime.now().strftime("%Y-%m-%d")
try:
data = client.get_spo2_data(date)
return {"spo2": data, "date": date}
except Exception as e:
return {"error": str(e), "date": date}
def fetch_respiration(client, date=None):
"""Fetch respiration data throughout the day."""
if not date:
date = datetime.now().strftime("%Y-%m-%d")
try:
data = client.get_respiration_data(date)
return {"respiration": data, "date": date}
except Exception as e:
return {"error": str(e), "date": date}
def fetch_steps_detailed(client, date=None):
"""Fetch detailed step data."""
if not date:
date = datetime.now().strftime("%Y-%m-%d")
try:
data = client.get_steps_data(date)
return {"steps": data, "date": date}
except Exception as e:
return {"error": str(e), "date": date}
def fetch_floors(client, date=None):
"""Fetch floors climbed data."""
if not date:
date = datetime.now().strftime("%Y-%m-%d")
try:
data = client.get_floors(date)
return {"floors": data, "date": date}
except Exception as e:
return {"error": str(e), "date": date}
def fetch_intensity_minutes(client, date=None):
"""Fetch intensity minutes (vigorous/moderate activity)."""
if not date:
date = datetime.now().strftime("%Y-%m-%d")
try:
data = client.get_intensity_minutes_data(date)
return {"intensity_minutes": data, "date": date}
except Exception as e:
return {"error": str(e), "date": date}
def fetch_hydration(client, date=None):
"""Fetch hydration/water intake data."""
if not date:
date = datetime.now().strftime("%Y-%m-%d")
try:
data = client.get_hydration_data(date)
return {"hydration": data, "date": date}
except Exception as e:
return {"error": str(e), "date": date}
def fetch_all_day_stress(client, date=None):
"""Fetch detailed stress data throughout the day (time-series)."""
if not date:
date = datetime.now().strftime("%Y-%m-%d")
try:
data = client.get_all_day_stress(date)
return {"stress_detailed": data, "date": date}
except Exception as e:
return {"error": str(e), "date": date}
def fetch_max_metrics(client, date=None):
"""Fetch max metrics (VO2 max, etc.)."""
if not date:
date = datetime.now().strftime("%Y-%m-%d")
try:
data = client.get_max_metrics(date)
return {"max_metrics": data, "date": date}
except Exception as e:
return {"error": str(e), "date": date}
def fetch_fitness_age(client):
"""Fetch fitness age data."""
try:
data = client.get_fitnessage_data()
return {"fitness_age": data}
except Exception as e:
return {"error": str(e)}
def fetch_endurance_score(client):
"""Fetch endurance score."""
try:
data = client.get_endurance_score()
return {"endurance_score": data}
except Exception as e:
return {"error": str(e)}
def fetch_hill_score(client):
"""Fetch hill score."""
try:
data = client.get_hill_score()
return {"hill_score": data}
except Exception as e:
return {"error": str(e)}
def fetch_intraday_heart_rate(client, date=None):
"""Fetch heart rate data throughout the day with timestamps."""
if not date:
date = datetime.now().strftime("%Y-%m-%d")
try:
# get_heart_rates returns time-series data
data = client.get_heart_rates(date)
return {"heart_rate_intraday": data, "date": date}
except Exception as e:
return {"error": str(e), "date": date}
def main():
import argparse
parser = argparse.ArgumentParser(description="Fetch extended Garmin health data")
parser.add_argument("metric", choices=[
"training_readiness", "training_status", "body_composition",
"weigh_ins", "spo2", "respiration", "steps", "floors",
"intensity_minutes", "hydration", "stress_detailed",
"max_metrics", "fitness_age", "endurance_score", "hill_score",
"hr_intraday"
], help="Type of data to fetch")
parser.add_argument("--date", help="Date (YYYY-MM-DD), defaults to today")
parser.add_argument("--start", help="Start date for date ranges (YYYY-MM-DD)")
parser.add_argument("--end", help="End date for date ranges (YYYY-MM-DD)")
args = parser.parse_args()
client = get_client()
if not client:
print('{"error": "Not authenticated"}')
sys.exit(1)
# Route to appropriate function
if args.metric == "training_readiness":
result = fetch_training_readiness(client, args.date)
elif args.metric == "training_status":
result = fetch_training_status(client, args.date)
elif args.metric == "body_composition":
result = fetch_body_composition(client, args.date)
elif args.metric == "weigh_ins":
result = fetch_weigh_ins(client, args.start, args.end)
elif args.metric == "spo2":
result = fetch_spo2(client, args.date)
elif args.metric == "respiration":
result = fetch_respiration(client, args.date)
elif args.metric == "steps":
result = fetch_steps_detailed(client, args.date)
elif args.metric == "floors":
result = fetch_floors(client, args.date)
elif args.metric == "intensity_minutes":
result = fetch_intensity_minutes(client, args.date)
elif args.metric == "hydration":
result = fetch_hydration(client, args.date)
elif args.metric == "stress_detailed":
result = fetch_all_day_stress(client, args.date)
elif args.metric == "max_metrics":
result = fetch_max_metrics(client, args.date)
elif args.metric == "fitness_age":
result = fetch_fitness_age(client)
elif args.metric == "endurance_score":
result = fetch_endurance_score(client)
elif args.metric == "hill_score":
result = fetch_hill_score(client)
elif args.metric == "hr_intraday":
result = fetch_intraday_heart_rate(client, args.date)
print(json.dumps(result, indent=2))
if __name__ == "__main__":
main()
FILE:scripts/garmin_query.py
#!/usr/bin/env python3
"""
Query Garmin data by time - "what was my heart rate at 3pm yesterday?"
"""
import json
import sys
from datetime import datetime, timedelta
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent))
from garmin_auth import get_client
def parse_time(time_str, date_str=None):
"""Parse various time formats into datetime."""
if not date_str:
date_str = datetime.now().strftime("%Y-%m-%d")
# Try different formats
formats = [
"%Y-%m-%d %H:%M:%S",
"%Y-%m-%d %H:%M",
"%H:%M:%S",
"%H:%M",
"%I:%M %p", # 3:00 PM
"%I %p", # 3 PM
]
for fmt in formats:
try:
if "%Y" in fmt:
return datetime.strptime(time_str, fmt)
else:
return datetime.strptime(f"{date_str} {time_str}", f"%Y-%m-%d {fmt}")
except ValueError:
continue
raise ValueError(f"Could not parse time: {time_str}")
def find_closest_datapoint(target_time, data_array, timestamp_key="startTimeInSeconds"):
"""Find the closest data point to a target time."""
if not data_array:
return None
target_timestamp = int(target_time.timestamp())
# Handle different timestamp formats
def get_timestamp(item):
if timestamp_key in item:
ts = item[timestamp_key]
# Handle milliseconds
if ts > 10000000000:
ts = ts // 1000
return ts
elif "startGMT" in item:
# ISO format or timestamp
start_gmt = item["startGMT"]
if isinstance(start_gmt, str):
return int(datetime.fromisoformat(start_gmt.replace("Z", "+00:00")).timestamp())
else:
return start_gmt // 1000 if start_gmt > 10000000000 else start_gmt
return None
closest = None
min_diff = float('inf')
for item in data_array:
ts = get_timestamp(item)
if ts is None:
continue
diff = abs(ts - target_timestamp)
if diff < min_diff:
min_diff = diff
closest = item
return closest
def query_heart_rate_at_time(client, time_str, date_str=None):
"""Get heart rate at a specific time."""
target_time = parse_time(time_str, date_str)
date = target_time.strftime("%Y-%m-%d")
try:
# Get intraday heart rate data
data = client.get_heart_rates(date)
if not data:
return {"error": "No heart rate data for this date", "date": date}
# Data format varies - try different keys
hr_array = data.get("heartRateValues") or data.get("allDayHR") or []
# heartRateValues format: [[timestamp_ms, hr_value], ...]
# Convert to dict format for our function
if hr_array and isinstance(hr_array[0], list):
hr_array = [{"startTimeInSeconds": ts//1000, "heartRateValue": val} for ts, val in hr_array]
closest = find_closest_datapoint(target_time, hr_array)
if closest:
return {
"requested_time": time_str,
"actual_time": datetime.fromtimestamp(
closest.get("startTimeInSeconds", 0)
).strftime("%Y-%m-%d %H:%M:%S"),
"heart_rate": closest.get("heartRateValue") or closest.get("value"),
"date": date
}
else:
return {"error": "No data point found near requested time", "date": date}
except Exception as e:
return {"error": str(e), "date": date}
def query_stress_at_time(client, time_str, date_str=None):
"""Get stress level at a specific time."""
target_time = parse_time(time_str, date_str)
date = target_time.strftime("%Y-%m-%d")
try:
data = client.get_all_day_stress(date)
if not data:
return {"error": "No stress data for this date", "date": date}
stress_values = data.get("stressValuesArray") or []
closest = find_closest_datapoint(target_time, stress_values)
if closest:
return {
"requested_time": time_str,
"stress_level": closest.get("stressLevel") or closest.get("value"),
"date": date
}
else:
return {"error": "No data point found near requested time", "date": date}
except Exception as e:
return {"error": str(e), "date": date}
def query_body_battery_at_time(client, time_str, date_str=None):
"""Get Body Battery level at a specific time."""
target_time = parse_time(time_str, date_str)
date = target_time.strftime("%Y-%m-%d")
try:
data = client.get_body_battery(date)
if not data or len(data) == 0:
return {"error": "No Body Battery data for this date", "date": date}
# Body Battery is in bodyBatteryValuesArray
bb_values = data[0].get("bodyBatteryValuesArray", [])
# Convert [timestamp, value] pairs to dicts
bb_dicts = [{"startTimeInSeconds": ts//1000, "value": val} for ts, val in bb_values]
closest = find_closest_datapoint(target_time, bb_dicts)
if closest:
return {
"requested_time": time_str,
"body_battery": closest.get("value"),
"date": date
}
else:
return {"error": "No data point found near requested time", "date": date}
except Exception as e:
return {"error": str(e), "date": date}
def query_steps_at_time(client, time_str, date_str=None):
"""Get step count at a specific time."""
target_time = parse_time(time_str, date_str)
date = target_time.strftime("%Y-%m-%d")
try:
data = client.get_steps_data(date)
if not data:
return {"error": "No steps data for this date", "date": date}
# Steps are usually cumulative throughout the day
step_values = data.get("stepsArray") or []
closest = find_closest_datapoint(target_time, step_values)
if closest:
return {
"requested_time": time_str,
"steps": closest.get("steps") or closest.get("value"),
"date": date
}
else:
return {"error": "No data point found near requested time", "date": date}
except Exception as e:
return {"error": str(e), "date": date}
def main():
import argparse
parser = argparse.ArgumentParser(description="Query Garmin data by time")
parser.add_argument("metric", choices=["heart_rate", "stress", "body_battery", "steps"],
help="Metric to query")
parser.add_argument("time", help="Time (e.g., '3:00 PM', '15:00', '2024-01-15 14:30')")
parser.add_argument("--date", help="Date if not included in time (YYYY-MM-DD)")
args = parser.parse_args()
client = get_client()
if not client:
print('{"error": "Not authenticated"}')
sys.exit(1)
if args.metric == "heart_rate":
result = query_heart_rate_at_time(client, args.time, args.date)
elif args.metric == "stress":
result = query_stress_at_time(client, args.time, args.date)
elif args.metric == "body_battery":
result = query_body_battery_at_time(client, args.time, args.date)
elif args.metric == "steps":
result = query_steps_at_time(client, args.time, args.date)
print(json.dumps(result, indent=2))
if __name__ == "__main__":
main()
Garmin Connect integration for Clawdbot: sync fitness data (steps, HR, calories, workouts, sleep) every 5 minutes using OAuth.
--- name: garmin-connect description: "Garmin Connect integration for Clawdbot: sync fitness data (steps, HR, calories, workouts, sleep) every 5 minutes using OAuth." --- # Garmin Connect Skill Sync all your Garmin fitness data to Clawdbot: - 🚶 **Daily Activity**: Steps, heart rate, calories, active minutes, distance - 😴 **Sleep**: Duration, quality, deep/REM/light sleep breakdown - 🏋️ **Workouts**: Recent activities with distance, duration, calories, heart rate - ⏱️ **Real-time sync**: Every 5 minutes via cron ## Quick Start ### 1. Install Dependencies ```bash pip install -r requirements.txt ``` ### 2. OAuth Authentication (One-time) ```bash python3 scripts/garmin-auth.py [email protected] your-password ``` This saves your OAuth session to `~/.garth/session.json` — fully local and secure. ### 3. Test Sync ```bash python3 scripts/garmin-sync.py ``` You should see JSON output with today's stats. ### 4. Set Up 5-Minute Cron Add to your crontab: ```bash */5 * * * * /home/user/garmin-connect-clawdbot/scripts/garmin-cron.sh ``` Or manually: ```bash */5 * * * * python3 /home/user/garmin-connect-clawdbot/scripts/garmin-sync.py ~/.clawdbot/.garmin-cache.json ``` ### 5. Use in Clawdbot Import and use in your scripts: ```python from scripts.garmin_formatter import format_all, get_as_dict # Get all formatted data print(format_all()) # Or get raw dict data = get_as_dict() print(f"Steps today: {data['summary']['steps']}") ``` ## Features ✅ OAuth-based (secure, no password storage) ✅ All metrics: activity, sleep, workouts ✅ Local caching (fast access) ✅ Cron-friendly (5-minute intervals) ✅ Easy Clawdbot integration ✅ Multi-user support ## Data Captured ### Daily Activity (`summary`) - `steps`: Daily step count - `heart_rate_resting`: Resting heart rate (bpm) - `calories`: Total calories burned - `active_minutes`: Intensity minutes - `distance_km`: Distance traveled ### Sleep (`sleep`) - `duration_hours`: Total sleep time - `duration_minutes`: Sleep in minutes - `quality_percent`: Sleep quality score (0-100) - `deep_sleep_hours`: Deep sleep duration - `rem_sleep_hours`: REM sleep duration - `light_sleep_hours`: Light sleep duration - `awake_minutes`: Time awake during sleep ### Workouts (`workouts`) For each recent workout: - `type`: Activity type (Running, Cycling, etc.) - `name`: Activity name - `distance_km`: Distance traveled - `duration_minutes`: Duration of activity - `calories`: Calories burned - `heart_rate_avg`: Average heart rate - `heart_rate_max`: Max heart rate ## Cache Location By default, data is cached at: `~/.clawdbot/.garmin-cache.json` Customize with: ```bash python3 scripts/garmin-sync.py /custom/path/cache.json ``` ## Files | File | Purpose | |------|---------| | `garmin-auth.py` | OAuth setup (run once) | | `garmin-sync.py` | Main sync logic (run every 5 min) | | `garmin-formatter.py` | Format data for display | | `garmin-cron.sh` | Cron wrapper script | | `requirements.txt` | Python dependencies | ## Troubleshooting ### OAuth authentication fails - Check email/password - Disable 2FA on Garmin account (or use app password) - Garmin servers might be rate-limiting — wait 5 minutes ### No data appears 1. Sync your Garmin device with the Garmin Connect app 2. Wait 2-3 minutes for data to sync 3. Check that data appears in Garmin Connect web/app 4. Then run `garmin-sync.py` again ### Permission denied on cron ```bash chmod +x scripts/garmin-cron.sh chmod +x scripts/garmin-sync.py chmod +x scripts/garmin-auth.py ``` ### Cache file not found Run `garmin-sync.py` at least once to create cache: ```bash python3 scripts/garmin-sync.py ``` ## Usage Examples ```python from scripts.garmin_formatter import format_all, get_as_dict # Get formatted output print(format_all()) # Get raw data data = get_as_dict() if data: print(f"Sleep: {data['sleep']['duration_hours']}h") print(f"Steps: {data['summary']['steps']:,}") ``` ## License MIT — Use, fork, modify freely. --- Made for [Clawdbot](https://clawd.bot) | Available on [ClawdHub](https://clawdhub.com) FILE:README.md # Garmin Connect Integration for Clawdbot Sync your Garmin fitness data (steps, HR, calories, workouts, sleep) automatically to Clawdbot every 5 minutes. ## Quick Start ### 1. Install Dependencies ```bash pip install -r requirements.txt ``` ### 2. Authenticate with OAuth Run the authentication script: ```bash python3 scripts/garmin-auth.py [email protected] your-password ``` This saves your OAuth session to `~/.garth/session.json` (local, secure). ⚠️ **Keep this file safe** — it contains your Garmin OAuth token. ### 3. Test ```bash python3 scripts/garmin-sync.py ``` You should see JSON output with your current Garmin data. ### 4. Set Up Cron (5-minute sync) Add to your crontab: ```bash */5 * * * * python3 /path/to/scripts/garmin-sync.py ``` ### 5. Use in Your Scripts Import Garmin data in any Clawdbot script: ```python from garmin_connect_clawdbot.scripts.garmin_formatter import format_all, get_as_dict # Get all formatted data all_data = format_all() print(all_data) # Or get raw dictionary data = get_as_dict() steps = data['summary']['steps'] sleep_hours = data['sleep']['duration_hours'] ``` ## Features - ✅ OAuth-based authentication (secure) - ✅ Real-time sync every 5 minutes - ✅ Sleep quality tracking (duration, deep/REM/light sleep) - ✅ Daily activity metrics (steps, HR, calories, distance) - ✅ Workout tracking (all activity types) - ✅ Body battery monitoring - ✅ Local caching (JSON) - ✅ Easy Clawdbot integration ## Scripts | Script | Purpose | |--------|---------| | `garmin-auth.py` | OAuth authentication (run once) | | `garmin-sync.py` | Sync all data from Garmin | | `garmin-formatter.py` | Format data for display | | `garmin-cron.sh` | Wrapper for cron jobs | ## Data Caching Data is cached locally in JSON format for quick access without constant API calls. ### Data Structure The cached data contains: - **summary**: Daily activity (steps, heart rate, calories, active minutes, distance) - **sleep**: Sleep metrics (duration, quality, deep/REM/light sleep breakdown) - **workouts**: Recent activities (type, distance, duration, calories, heart rate) ## Usage Examples ### Format All Data ```python from scripts.garmin_formatter import format_all output = format_all() # Returns formatted string print(output) ``` ### Access Raw Data ```python from scripts.garmin_formatter import get_as_dict data = get_as_dict() if data: print(f"Sleep: {data['sleep']['duration_hours']}h") print(f"Steps: {data['summary']['steps']:,}") print(f"Workouts: {len(data['workouts'])} activities") ``` ### Format Specific Metrics ```python from scripts.garmin_formatter import format_daily_summary, format_sleep, format_workouts # Use individually print(format_daily_summary()) print(format_sleep()) print(format_workouts()) ``` ## Troubleshooting ### Authentication Failed - Check email/password - Ensure 2FA is **OFF** on Garmin (or use app-specific password) - Garmin servers might rate-limit — wait 5 minutes ### No Sleep Data - Sync your Garmin device with the Garmin Connect app - Sleep must be tracked while wearing device - Data available ~1 hour after waking ### Missing Workouts - Sync device → Garmin Connect app - Confirm workout saved in Garmin app - Check `garmin-sync.py` logs ### Data not syncing Run the sync script manually: ```bash python3 scripts/garmin-sync.py ``` Check the output for errors. ## License MIT — Use freely, fork, modify as needed. --- Made for [Clawdbot](https://clawd.bot) | Available on [ClawdHub](https://clawdhub.com) FILE:_meta.json { "ownerId": "kn7c0sy4fbf4n92n3nm242axc1800bgk", "slug": "lovefromio-garmin-connect", "version": "1.0.1", "publishedAt": 1769535574161, "forkOf": "rayleigh3105/[email protected]" } FILE:requirements.txt garminconnect>=0.2.38 requests>=2.28.0 python-dateutil>=2.8.2 FILE:scripts/garmin-auth-oauth.py #!/usr/bin/env python3 """ Garmin Connect OAuth Authentication (NEW) Garmin switched to OAuth cookie-based auth """ import os import json from pathlib import Path try: from garth import Client except ImportError: print("❌ garth not installed. Run: pip install garth") exit(1) def get_oauth_client(): """Get authenticated Garmin client with OAuth""" # OAuth cache file garth_cache_dir = Path.home() / ".garth" garth_cache_dir.mkdir(exist_ok=True) client = Client() # Try to load existing token try: client.load("/home/mamotec/.garth/session.json") print("✅ Loaded existing Garmin OAuth session") return client except: pass # Need to authenticate - this requires browser login print("⚠️ First-time setup: Need to authenticate via browser") print("\nGarmin switched to OAuth. Follow these steps:") print("1. Run: python3 garmin-auth-oauth.py") print("2. Open: https://sso.garmin.com/sso/signin") print("3. Sign in with your Garmin account") print("4. The script will save your session for future use") return None def save_session(client): """Save OAuth session for future use""" client.dump("/home/mamotec/.garth/session.json") print("✅ Session saved to ~/.garth/session.json") if __name__ == "__main__": print("Garmin OAuth Setup") print("=" * 50) # Try to load existing session try: client = Client() client.load("/home/mamotec/.garth/session.json") print("✅ OAuth session already configured") print("Try: python3 garmin-sync.py") except: print("❌ No OAuth session found") print("\nManuelle Authentifizierung nötig:") print("1. Geh zu: https://sso.garmin.com/sso/signin") print("2. Melde dich an ([email protected])") print("3. Cookies werden automatisch gespeichert") print("\nOder nutze garth-cli:") print("pip install garth-cli") print("garth auth [email protected]") FILE:scripts/garmin-auth.py #!/usr/bin/env python3 """ Garmin OAuth Authentication Setup One-time setup: saves OAuth session for future use """ import os from pathlib import Path try: from garth import Client except ImportError: print("❌ garth not installed. Run: pip install garth") exit(1) def setup_oauth(email, password): """Authenticate with Garmin and save session""" garth_dir = Path.home() / ".garth" session_file = garth_dir / "session.json" print(f"🔐 Authenticating with Garmin ({email})...") try: client = Client() client.login(email, password) # Save session garth_dir.mkdir(exist_ok=True) client.dump(str(session_file)) print(f"✅ OAuth session saved to {session_file}") print("✅ You can now use garmin-sync.py") return True except Exception as e: print(f"❌ Authentication failed: {e}") print("\nCommon issues:") print("- Wrong email/password") print("- 2FA enabled (disable or use app-specific password)") print("- Garmin servers temporary unavailable") return False if __name__ == "__main__": import sys if len(sys.argv) < 3: print("Usage: python3 garmin-auth.py <email> <password>") print("Example: python3 garmin-auth.py [email protected] MyPassword123") sys.exit(1) email = sys.argv[1] password = sys.argv[2] success = setup_oauth(email, password) sys.exit(0 if success else 1) FILE:scripts/garmin-cron.sh #!/bin/bash # Cron wrapper for 5-minute Garmin sync checks # Add to crontab: */5 * * * * /home/mamotec/clawd/garmin-connect-clawdbot/scripts/garmin-cron.sh cd /home/mamotec/clawd/garmin-connect-clawdbot # Run sync (with timeout to prevent hanging) timeout 30 python3 scripts/garmin-sync.py > /tmp/garmin-sync.log 2>&1 # Check for significant changes and alert if needed # (Would send to Telegram via Clawdbot message tool) exit 0 FILE:scripts/garmin-formatter.py #!/usr/bin/env python3 """ Format Garmin data for display Provides formatted output for integration into Clawdbot """ import json import os from pathlib import Path def load_cache(): """Load latest Garmin data from cache""" cache_file = Path.home() / ".clawdbot" / ".garmin-cache.json" if not cache_file.exists(): return None try: with open(cache_file, 'r') as f: return json.load(f) except: return None def format_daily_summary(): """Format daily activity summary""" data = load_cache() if not data: return "📊 No Garmin data available yet. Run sync first." summary = data.get('summary', {}) output = "📊 **Daily Activity**\n" output += f"🚶 Steps: {summary.get('steps', 0):,}\n" output += f"❤️ Heart Rate (resting): {summary.get('heart_rate_resting', 0)} bpm\n" output += f"🔥 Calories: {summary.get('calories', 0):,}\n" output += f"⏱️ Active Minutes: {summary.get('active_minutes', 0)} min\n" output += f"📏 Distance: {summary.get('distance_km', 0)} km" return output def format_sleep(): """Format sleep data""" data = load_cache() if not data: return "😴 No sleep data available" sleep = data.get('sleep', {}) quality = sleep.get('quality_percent', 0) quality_emoji = "😴" if quality >= 80 else "😐" if quality >= 60 else "😩" output = f"{quality_emoji} **Sleep**\n" output += f"Duration: {sleep.get('duration_hours', 0)}h {int(sleep.get('duration_minutes', 0) % 60)}m\n" output += f"Quality: {quality}%\n" output += f"Deep Sleep: {sleep.get('deep_sleep_hours', 0)}h\n" output += f"REM Sleep: {sleep.get('rem_sleep_hours', 0)}h\n" output += f"Light Sleep: {sleep.get('light_sleep_hours', 0)}h\n" output += f"Awake: {int(sleep.get('awake_minutes', 0))} min" return output def format_workouts(): """Format recent workouts""" data = load_cache() if not data: return "🏋️ No workout data available" workouts = data.get('workouts', []) if not workouts: return "🏋️ No recent workouts" output = "🏋️ **Recent Workouts**\n" for workout in workouts[:5]: # Show last 5 output += f"\n• {workout.get('type', 'Workout')}: {workout.get('name', 'Unnamed')}\n" output += f" Distance: {workout.get('distance_km', 0)} km\n" output += f" Duration: {int(workout.get('duration_minutes', 0))} min\n" output += f" Calories: {workout.get('calories', 0)}\n" output += f" HR: {workout.get('heart_rate_avg', 0)} avg / {workout.get('heart_rate_max', 0)} max" return output def format_all(): """Format all data at once""" output = "" output += format_daily_summary() + "\n\n" output += format_sleep() + "\n\n" output += format_workouts() return output def get_as_dict(): """Get all data as dict (for Clawdbot integration)""" return load_cache() if __name__ == "__main__": print(format_all()) FILE:scripts/garmin-sync-30days.py #!/usr/bin/env python3 """ Garmin Connect 30-Day Health Data Sync Fetches 30 days of complete health data and generates interactive dashboard """ import json import os import sys from datetime import datetime, timedelta from pathlib import Path try: from garth import Client from garminconnect import Garmin except ImportError as e: print(f"❌ Dependencies missing: {e}") print("Run: pip install garth garminconnect") sys.exit(1) def load_garth_session(): """Load saved Garmin OAuth session""" session_file = Path.home() / ".garth" / "session.json" if not session_file.exists(): print(f"❌ No OAuth session found at {session_file}") print("\nRun: python3 garmin-auth.py <email> <password>") return None try: client = Client() client.load(str(session_file)) return client except Exception as e: print(f"❌ Failed to load session: {e}") return None def get_daily_summary(garth_client, date_str): """Get daily summary with retry logic for 429 errors""" import time max_retries = 3 retry_delays = [60, 120, 240] # Exponential backoff for attempt in range(max_retries): try: gc = Garmin() gc.garth = garth_client summary = gc.get_user_summary(date_str) return { 'steps': summary.get('totalSteps', 0), 'heart_rate_resting': summary.get('restingHeartRate', 0), 'calories': summary.get('totalKilocalories', 0), 'active_minutes': summary.get('totalIntensityMinutes', 0), 'distance_km': round(summary.get('totalDistance', 0) / 1000, 2), } except Exception as e: if "429" in str(e) and attempt < max_retries - 1: wait = retry_delays[attempt] print(f"⚠️ Rate limited (429). Waiting {wait}s before retry...") time.sleep(wait) continue elif "403" in str(e): # Try alternate endpoint try: # Use daily summary endpoint directly gc = Garmin() gc.garth = garth_client # Try get daily stats via different method stats = gc.get_stats(date_str) return { 'steps': stats.get('dailySteps', 0), 'heart_rate_resting': stats.get('restingHeartRate', 0), 'calories': stats.get('totalKilocalories', 0), 'active_minutes': stats.get('totalIntensityMinutes', 0), 'distance_km': 0, } except: print(f"⚠️ Daily summary error for {date_str}: {e}") return None else: print(f"⚠️ Daily summary error for {date_str}: {e}") return None return None def get_sleep_data(garth_client, date_str): """Get sleep data for a specific date""" try: gc = Garmin() gc.garth = garth_client sleep = gc.get_sleep_data(date_str) if sleep and 'dailySleepDTO' in sleep: s = sleep['dailySleepDTO'] duration_sec = s.get('sleepTimeSeconds', 0) return { 'date': date_str, 'duration_hours': round(duration_sec / 3600, 1), 'duration_minutes': round(duration_sec / 60, 0), 'quality_percent': s.get('sleepQualityPercentage', 0), 'deep_sleep_hours': round(s.get('deepSleepSeconds', 0) / 3600, 1), 'rem_sleep_hours': round(s.get('remSleepSeconds', 0) / 3600, 1), 'light_sleep_hours': round(s.get('lightSleepSeconds', 0) / 3600, 1), 'awake_minutes': round(s.get('awakeTimeSeconds', 0) / 60, 0), } except Exception as e: print(f"⚠️ Sleep data error for {date_str}: {e}") return { 'date': date_str, 'duration_hours': 0, 'quality_percent': 0, 'deep_sleep_hours': 0, 'rem_sleep_hours': 0, 'light_sleep_hours': 0, 'awake_minutes': 0, } def get_heart_rate(garth_client, date_str): """Get heart rate data""" try: gc = Garmin() gc.garth = garth_client hr_data = gc.get_heart_rate(date_str) if hr_data: return { 'resting': hr_data.get('restingHeartRate', 0), 'max': hr_data.get('maxHeartRate', 0), 'avg': hr_data.get('averageHeartRate', 0), } except Exception as e: print(f"⚠️ Heart rate error for {date_str}: {e}") return {'resting': 0, 'max': 0, 'avg': 0} def get_workouts(garth_client, days=30): """Get recent workouts from the last N days""" workouts = [] try: gc = Garmin() gc.garth = garth_client # Get activities from last 30 days start_date = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d") activities = gc.get_activities(start_date, datetime.now().strftime("%Y-%m-%d")) for activity in activities: workout = { 'date': activity.get('startTimeLocal', ''), 'type': activity.get('activityType', {}).get('typeKey', 'Unknown'), 'name': activity.get('activityName', 'Unnamed'), 'distance_km': round(activity.get('distance', 0) / 1000, 2), 'duration_minutes': round(activity.get('duration', 0) / 60, 0), 'calories': activity.get('calories', 0), 'heart_rate_avg': activity.get('avgHeartRate', 0), 'heart_rate_max': activity.get('maxHeartRate', 0), } workouts.append(workout) except Exception as e: print(f"⚠️ Workouts error: {e}") return workouts def sync_30_days(): """Sync 30 days of Garmin health data""" garth_client = load_garth_session() if not garth_client: return None today = datetime.now() thirty_days_ago = today - timedelta(days=30) print(f"📊 Fetching 30 days of health data ({thirty_days_ago.strftime('%Y-%m-%d')} to {today.strftime('%Y-%m-%d')})...") daily_data = [] current = thirty_days_ago while current <= today: date_str = current.strftime("%Y-%m-%d") print(f" Fetching {date_str}...", end="", flush=True) summary = get_daily_summary(garth_client, date_str) sleep = get_sleep_data(garth_client, date_str) hr = get_heart_rate(garth_client, date_str) day_data = { 'date': date_str, 'summary': summary or {}, 'sleep': sleep, 'heart_rate': hr, } daily_data.append(day_data) print(" ✓") current += timedelta(days=1) # Get workouts (last 30 days) print("\n🏃 Fetching workouts...") workouts = get_workouts(garth_client, 30) # Compile all data all_data = { 'timestamp': datetime.now().isoformat(), 'period': { 'start': thirty_days_ago.strftime("%Y-%m-%d"), 'end': today.strftime("%Y-%m-%d"), 'days': 30, }, 'daily': daily_data, 'workouts': workouts, 'summary_stats': calculate_summary_stats(daily_data, workouts), } return all_data def calculate_summary_stats(daily_data, workouts): """Calculate aggregated statistics for the 30-day period""" total_steps = sum(d.get('summary', {}).get('steps', 0) for d in daily_data if d.get('summary')) avg_steps = total_steps / len(daily_data) if daily_data else 0 total_calories = sum(d.get('summary', {}).get('calories', 0) for d in daily_data if d.get('summary')) total_active_minutes = sum(d.get('summary', {}).get('active_minutes', 0) for d in daily_data if d.get('summary')) avg_sleep = sum(d.get('sleep', {}).get('duration_hours', 0) for d in daily_data) / len(daily_data) if daily_data else 0 avg_sleep_quality = sum(d.get('sleep', {}).get('quality_percent', 0) for d in daily_data) / len(daily_data) if daily_data else 0 total_workouts = len(workouts) total_workout_minutes = sum(w.get('duration_minutes', 0) for w in workouts) total_workout_calories = sum(w.get('calories', 0) for w in workouts) return { 'total_steps': total_steps, 'avg_daily_steps': round(avg_steps, 0), 'total_calories': total_calories, 'total_active_minutes': total_active_minutes, 'avg_sleep_hours': round(avg_sleep, 1), 'avg_sleep_quality': round(avg_sleep_quality, 1), 'total_workouts': total_workouts, 'total_workout_minutes': round(total_workout_minutes, 0), 'total_workout_calories': total_workout_calories, } def generate_dashboard_html(data, output_path): """Generate an interactive HTML health dashboard""" html_template = """<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Garmin Health Dashboard - Last 30 Days</title> <script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <style> .metric-card { transition: transform 0.2s; } .metric-card:hover { transform: translateY(-4px); } </style> </head> <body class="bg-gradient-to-br from-slate-900 via-blue-900 to-slate-900 min-h-screen text-white p-6"> <div class="max-w-7xl mx-auto"> <!-- Header --> <div class="text-center mb-8"> <h1 class="text-4xl font-bold mb-2">🏃 Garmin Health Dashboard</h1> <p class="text-blue-300">30-Day Summary | {start} → {end}</p> <p class="text-sm text-gray-400 mt-2">Generated: {timestamp}</p> </div> <!-- Key Metrics --> <div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8"> <div class="metric-card bg-gradient-to-br from-blue-600 to-blue-800 p-6 rounded-xl shadow-lg"> <div class="text-blue-200 text-sm">Total Steps</div> <div class="text-3xl font-bold">{total_steps:,}</div> <div class="text-blue-200 text-sm">Daily avg: {avg_steps:,}</div> </div> <div class="metric-card bg-gradient-to-br from-green-600 to-green-800 p-6 rounded-xl shadow-lg"> <div class="text-green-200 text-sm">Total Calories</div> <div class="text-3xl font-bold">{total_calories:,}</div> <div class="text-green-200 text-sm">Active: {active_minutes} min</div> </div> <div class="metric-card bg-gradient-to-br from-purple-600 to-purple-800 p-6 rounded-xl shadow-lg"> <div class="text-purple-200 text-sm">Avg Sleep</div> <div class="text-3xl font-bold">{avg_sleep}h</div> <div class="text-purple-200 text-sm">Quality: {avg_quality}%</div> </div> <div class="metric-card bg-gradient-to-br from-orange-600 to-orange-800 p-6 rounded-xl shadow-lg"> <div class="text-orange-200 text-sm">Workouts</div> <div class="text-3xl font-bold">{total_workouts}</div> <div class="text-orange-200 text-sm">{total_workout_minutes} min • {workout_calories} cal</div> </div> </div> <!-- Charts Row --> <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8"> <!-- Steps Chart --> <div class="bg-slate-800/50 backdrop-blur p-6 rounded-xl border border-slate-700"> <h2 class="text-xl font-bold mb-4">📈 Daily Steps</h2> <canvas id="stepsChart" height="200"></canvas> </div> <!-- Sleep Chart --> <div class="bg-slate-800/50 backdrop-blur p-6 rounded-xl border border-slate-700"> <h2 class="text-xl font-bold mb-4">😴 Sleep Duration</h2> <canvas id="sleepChart" height="200"></canvas> </div> </div> <!-- Second Row Charts --> <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8"> <!-- Heart Rate --> <div class="bg-slate-800/50 backdrop-blur p-6 rounded-xl border border-slate-700"> <h2 class="text-xl font-bold mb-4">❤️ Resting Heart Rate</h2> <canvas id="hrChart" height="200"></canvas> </div> <!-- Activity Types --> <div class="bg-slate-800/50 backdrop-blur p-6 rounded-xl border border-slate-700"> <h2 class="text-xl font-bold mb-4">🏋️ Workout Distribution</h2> <canvas id="workoutChart" height="200"></canvas> </div> </div> <!-- Workout List --> <div class="bg-slate-800/50 backdrop-blur p-6 rounded-xl border border-slate-700 mb-8"> <h2 class="text-xl font-bold mb-4">📋 Recent Workouts (Last 30 Days)</h2> <div class="overflow-x-auto"> <table class="w-full text-left"> <thead class="border-b border-slate-600"> <tr> <th class="py-3 px-4">Date</th> <th class="py-3 px-4">Type</th> <th class="py-3 px-4">Name</th> <th class="py-3 px-4">Duration</th> <th class="py-3 px-4">Distance</th> <th class="py-3 px-4">Calories</th> <th class="py-3 px-4">HR Avg/Max</th> </tr> </thead> <tbody> {workout_rows} </tbody> </table> </div> </div> <!-- Raw Data Section --> <details class="bg-slate-800/30 rounded-xl border border-slate-700 mb-8"> <summary class="p-4 cursor-pointer hover:bg-slate-800/50">📦 Raw JSON Data (Click to expand)</summary> <pre class="p-4 text-xs overflow-x-auto text-gray-300">{json_data}</pre> </details> </div> <script> // Chart.js configurations const dates = {dates}; const stepsData = {steps_data}; const sleepData = {sleep_data}; const hrData = {hr_data}; const workoutTypes = {workout_types}; const workoutCounts = {workout_counts}; // Steps Chart new Chart(document.getElementById('stepsChart'), {{ type: 'bar', data: {{ labels: dates, datasets: [{{ label: 'Steps', data: stepsData, backgroundColor: 'rgba(59, 130, 246, 0.7)', borderColor: 'rgba(59, 130, 246, 1)', borderWidth: 1 }}] }}, options: {{ responsive: true, maintainAspectRatio: false, plugins: {{ legend: {{ display: false }} }}, scales: {{ y: {{ beginAtZero: true, grid: {{ color: 'rgba(255,255,255,0.1)' }} }}, x: {{ grid: {{ display: false }} }} }} }} }}); // Sleep Chart new Chart(document.getElementById('sleepChart'), {{ type: 'line', data: {{ labels: dates, datasets: [{{ label: 'Sleep (hours)', data: sleepData, borderColor: 'rgba(168, 85, 247, 1)', backgroundColor: 'rgba(168, 85, 247, 0.1)', fill: true, tension: 0.4 }}] }}, options: {{ responsive: true, maintainAspectRatio: false, plugins: {{ legend: {{ display: false }} }}, scales: {{ y: {{ beginAtZero: true, grid: {{ color: 'rgba(255,255,255,0.1)' }} }}, x: {{ grid: {{ display: false }} }} }} }} }}); // Heart Rate Chart new Chart(document.getElementById('hrChart'), {{ type: 'line', data: {{ labels: dates, datasets: [{{ label: 'Resting HR', data: hrData, borderColor: 'rgba(239, 68, 68, 1)', backgroundColor: 'rgba(239, 68, 68, 0.1)', fill: true, tension: 0.4 }}] }}, options: {{ responsive: true, maintainAspectRatio: false, plugins: {{ legend: {{ display: false }} }}, scales: {{ y: {{ beginAtZero: false, grid: {{ color: 'rgba(255,255,255,0.1)' }} }}, x: {{ grid: {{ display: false }} }} }} }} }}); // Workout Types Chart new Chart(document.getElementById('workoutChart'), {{ type: 'doughnut', data: {{ labels: workoutTypes, datasets: [{{ data: workoutCounts, backgroundColor: [ 'rgba(59, 130, 246, 0.8)', 'rgba(16, 185, 129, 0.8)', 'rgba(245, 158, 11, 0.8)', 'rgba(239, 68, 68, 0.8)', 'rgba(168, 85, 247, 0.8)', 'rgba(236, 72, 153, 0.8)', ] }}] }}, options: {{ responsive: true, maintainAspectRatio: false, plugins: {{ legend: {{ position: 'bottom' }} }} }} }}); </script> </body> </html>""" # Prepare data for template dates = [d['date'] for d in data['daily']] steps_data = [d.get('summary', {}).get('steps', 0) for d in data['daily']] sleep_data = [d.get('sleep', {}).get('duration_hours', 0) for d in data['daily']] hr_data = [d.get('heart_rate', {}).get('resting', 0) for d in data['daily']] # Workout types distribution workout_type_counts = {} for w in workouts: t = w.get('type', 'Unknown') workout_type_counts[t] = workout_type_counts.get(t, 0) + 1 workout_types = list(workout_type_counts.keys()) workout_counts = list(workout_type_counts.values()) # Format workout rows workout_rows = "" for w in sorted(workouts, key=lambda x: x.get('date', ''), reverse=True)[:30]: hr_avg = w.get('heart_rate_avg', 0) hr_max = w.get('heart_rate_max', 0) workout_rows += f""" <tr class="border-b border-slate-700 hover:bg-slate-700/30"> <td class="py-3 px-4">{w.get('date', 'N/A')[:10]}</td> <td class="py-3 px-4 capitalize">{w.get('type', 'Unknown')}</td> <td class="py-3 px-4">{w.get('name', 'Unnamed')}</td> <td class="py-3 px-4">{w.get('duration_minutes', 0)} min</td> <td class="py-3 px-4">{w.get('distance_km', 0)} km</td> <td class="py-3 px-4">{w.get('calories', 0)} cal</td> <td class="py-3 px-4">{hr_avg}/{hr_max} bpm</td> </tr>""" # Render template html = html_template.format( start=data['period']['start'], end=data['period']['end'], timestamp=data['timestamp'], total_steps=data['summary_stats']['total_steps'], avg_steps=int(data['summary_stats']['avg_daily_steps']), total_calories=data['summary_stats']['total_calories'], active_minutes=data['summary_stats']['total_active_minutes'], avg_sleep=data['summary_stats']['avg_sleep_hours'], avg_quality=data['summary_stats']['avg_sleep_quality'], total_workouts=data['summary_stats']['total_workouts'], total_workout_minutes=data['summary_stats']['total_workout_minutes'], workout_calories=data['summary_stats']['total_workout_calories'], dates=json.dumps(dates), steps_data=json.dumps(steps_data), sleep_data=json.dumps(sleep_data), hr_data=json.dumps(hr_data), workout_types=json.dumps(workout_types), workout_counts=json.dumps(workout_counts), workout_rows=workout_rows, json_data=json.dumps(data, indent=2) ) # Write HTML file with open(output_path, 'w') as f: f.write(html) print(f"✅ Dashboard generated: {output_path}") if __name__ == "__main__": print("🚀 Starting 30-Day Garmin Health Sync") print("=" * 60) # Check dependencies first try: import garth import garminconnect except ImportError as e: print(f"❌ Missing dependency: {e}") print("\nInstall with: pip install garth garminconnect") sys.exit(1) # Sync data data = sync_30_days() if not data: print("❌ Failed to sync data") sys.exit(1) print("\n✅ 30-day sync complete!") print(f" 📊 Days fetched: {len(data['daily'])}") print(f" 🏃 Workouts: {len(data['workouts'])}") print(f" 📈 Total steps: {data['summary_stats']['total_steps']:,}") # Save raw data timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") data_file = f"/tmp/garmin_30days_{timestamp}.json" with open(data_file, 'w') as f: json.dump(data, f, indent=2) print(f" 💾 Raw data saved: {data_file}") # Generate dashboard dashboard_file = f"/tmp/garmin_dashboard_{timestamp}.html" generate_dashboard_html(data, dashboard_file) print(f" 📊 Dashboard generated: {dashboard_file}") # Also save latest reference latest_json = "/tmp/garmin_latest.json" latest_html = "/tmp/garmin_latest.html" with open(latest_json, 'w') as f: json.dump(data, f, indent=2) with open(latest_html, 'w') as f: with open(dashboard_file, 'r') as src: f.write(src.read()) print(f" 🔗 Latest links updated: {latest_json}, {latest_html}") print("\n✅ All tasks completed successfully!") FILE:scripts/garmin-sync-oauth.py #!/usr/bin/env python3 """ Garmin Connect Data Sync (OAuth version) NEW: Garmin now uses OAuth instead of username/password """ import json import os from datetime import datetime import sys try: from garth import Client except ImportError: print("❌ garth not installed. Run: pip install garth garminconnect") sys.exit(1) def get_garth_client(): """Get authenticated Garmin client using Garth (OAuth)""" garth_cache_dir = os.path.expanduser("~/.garth") cache_file = os.path.join(garth_cache_dir, "session.json") client = Client() # Try to load existing session if os.path.exists(cache_file): try: client.load(cache_file) return client except Exception as e: print(f"⚠️ Session expired, need to re-authenticate: {e}") print("❌ No OAuth session found") print("\nGarmin now requires OAuth authentication (browser-based)") print("\nSetup instructions:") print("1. Install garth CLI: pip install garth-cli") print("2. Run: garth auth [email protected]") print("3. Follow the browser prompt to sign in") print("4. Come back here and run: python3 garmin-sync-oauth.py") sys.exit(1) def sync_all_data(): """Sync all Garmin data using OAuth""" client = get_garth_client() if not client: return None # Load config for cache location config_path = os.path.expanduser('~/.clawdbot/garmin-config.json') cache_file = '/tmp/garmin-cache.json' if os.path.exists(config_path): with open(config_path, 'r') as f: config = json.load(f) cache_file = config.get('cache_file', cache_file) today = datetime.now().date() data = { 'timestamp': datetime.now().isoformat(), 'date': str(today), } try: # Get user summary summary = client.get_user_summary(today.isoformat()) data['summary'] = { 'steps': summary.get('totalSteps', 0), 'heart_rate': summary.get('heartRateData', {}).get('restingHeartRate', 0), 'calories': summary.get('totalKilocalories', 0), 'active_minutes': summary.get('totalIntensityMinutes', 0), 'distance_km': round(summary.get('totalDistance', 0) / 1000, 2), } # Get sleep data try: sleep_data = client.get_sleep_data(today.isoformat()) if sleep_data: data['sleep'] = { 'duration_hours': round(sleep_data.get('duration', 0) / 3600, 1), 'quality': sleep_data.get('qualityScore', 0), 'deep_sleep': round(sleep_data.get('deepSleepSeconds', 0) / 3600, 1), 'rem_sleep': round(sleep_data.get('remSleepSeconds', 0) / 3600, 1), 'light_sleep': round(sleep_data.get('lightSleepSeconds', 0) / 3600, 1), } else: data['sleep'] = {'duration_hours': 0, 'quality': 0} except: data['sleep'] = {'duration_hours': 0, 'quality': 0} # Get heart rate try: hr = summary.get('heartRateData', {}) data['heart_rate_details'] = { 'resting': hr.get('restingHeartRate', 0), 'max': hr.get('maxHeartRate', 0), } except: data['heart_rate_details'] = {} # Get activities/workouts try: activities = client.get_activities(0, 1) data['workouts'] = [] if activities: for a in activities[:5]: data['workouts'].append({ 'type': a.get('activityType', 'Unknown'), 'distance_km': round(a.get('distance', 0) / 1000, 2), 'duration_min': round(a.get('duration', 0) / 60, 1), 'calories': a.get('calories', 0), }) except: data['workouts'] = [] # Save cache os.makedirs(os.path.dirname(cache_file), exist_ok=True) with open(cache_file, 'w') as f: json.dump(data, f, indent=2) print(json.dumps(data, indent=2)) return data except Exception as e: print(f"❌ Sync error: {e}", file=sys.stderr) sys.exit(1) if __name__ == "__main__": sync_all_data() FILE:scripts/garmin-sync.py #!/usr/bin/env python3 """ Garmin Connect Data Sync Syncs all fitness data using saved OAuth session """ import json import os import sys from datetime import datetime, timedelta from pathlib import Path try: from garth import Client from garminconnect import Garmin except ImportError: print("❌ Dependencies not installed. Run: pip install -r requirements.txt") sys.exit(1) def load_garth_session(): """Load saved Garmin OAuth session""" session_file = Path.home() / ".garth" / "session.json" if not session_file.exists(): print(f"❌ No OAuth session found at {session_file}") print("\nRun: python3 garmin-auth.py <email> <password>") return None try: client = Client() client.load(str(session_file)) return client except Exception as e: print(f"❌ Failed to load session: {e}") return None def get_daily_summary(garth_client, date_str): """Get daily summary: steps, HR, calories, active minutes""" data = { 'steps': 0, 'heart_rate_resting': 0, 'calories': 0, 'active_minutes': 0, 'distance_km': 0, } try: # Use Garminconnect with garth session gc = Garmin() gc.garth = garth_client summary = gc.get_user_summary(date_str) data['steps'] = summary.get('totalSteps', 0) data['heart_rate_resting'] = summary.get('restingHeartRate', 0) data['calories'] = summary.get('totalKilocalories', 0) data['active_minutes'] = summary.get('totalIntensityMinutes', 0) data['distance_km'] = round(summary.get('totalDistance', 0) / 1000, 2) except Exception as e: print(f"⚠️ Daily summary error: {e}", file=sys.stderr) return data def get_sleep_data(garth_client, date_str): """Get sleep data: duration, quality, deep/REM sleep""" data = { 'duration_hours': 0, 'duration_minutes': 0, 'quality_percent': 0, 'deep_sleep_hours': 0, 'rem_sleep_hours': 0, 'light_sleep_hours': 0, 'awake_minutes': 0, } try: gc = Garmin() gc.garth = garth_client sleep = gc.get_sleep_data(date_str) if sleep and 'dailySleepDTO' in sleep: s = sleep['dailySleepDTO'] duration_sec = s.get('sleepTimeSeconds', 0) data['duration_hours'] = round(duration_sec / 3600, 1) data['duration_minutes'] = round(duration_sec / 60, 0) data['quality_percent'] = s.get('sleepQualityPercentage', 0) data['deep_sleep_hours'] = round(s.get('deepSleepSeconds', 0) / 3600, 1) data['rem_sleep_hours'] = round(s.get('remSleepSeconds', 0) / 3600, 1) data['light_sleep_hours'] = round(s.get('lightSleepSeconds', 0) / 3600, 1) data['awake_minutes'] = round(s.get('awakeTimeSeconds', 0) / 60, 0) except Exception as e: print(f"⚠️ Sleep data error: {e}", file=sys.stderr) return data def get_workouts(garth_client): """Get recent workouts""" workouts = [] try: gc = Garmin() gc.garth = garth_client activities = gc.get_activities(0, 20) # Last 20 workouts for activity in activities[:10]: # Return last 10 workout = { 'type': activity.get('activityType', 'Unknown'), 'name': activity.get('activityName', 'Unnamed'), 'distance_km': round(activity.get('distance', 0) / 1000, 2), 'duration_minutes': round(activity.get('duration', 0) / 60, 0), 'calories': activity.get('calories', 0), 'heart_rate_avg': activity.get('avgHeartRate', 0), 'heart_rate_max': activity.get('maxHeartRate', 0), 'timestamp': activity.get('startTimeInSeconds', 0), } workouts.append(workout) except Exception as e: print(f"⚠️ Workouts error: {e}", file=sys.stderr) return workouts def sync_all(output_file=None): """Sync all Garmin data""" garth_client = load_garth_session() if not garth_client: return None today = datetime.now().strftime("%Y-%m-%d") # Collect all data all_data = { 'timestamp': datetime.now().isoformat(), 'date': today, 'summary': get_daily_summary(garth_client, today), 'sleep': get_sleep_data(garth_client, today), 'workouts': get_workouts(garth_client), } # Save to file if specified if output_file: os.makedirs(os.path.dirname(output_file), exist_ok=True) with open(output_file, 'w') as f: json.dump(all_data, f, indent=2) # Print JSON to stdout print(json.dumps(all_data, indent=2)) return all_data if __name__ == "__main__": # Default cache file cache_file = os.path.expanduser('~/.clawdbot/.garmin-cache.json') # Use custom path if provided if len(sys.argv) > 1: cache_file = sys.argv[1] sync_all(cache_file)
Ultimate AI agent memory system. WAL protocol + vector search + git-notes + cloud backup. Never lose context again. Works with Claude, Cursor, GPT, OpenClaw...
---
name: elite-longterm-memory
version: 1.2.1
description: "Ultimate AI agent memory system. WAL protocol + vector search + git-notes + cloud backup. Never lose context again. Works with Claude, Cursor, GPT, OpenClaw agents."
author: NextFrontierBuilds
keywords: [memory, ai-agent, ai-coding, long-term-memory, vector-search, lancedb, git-notes, wal, persistent-context, claude, claude-code, gpt, cursor, copilot, openclaw, moltbot, openclaw, vibe-coding, agentic]
metadata:
openclaw:
emoji: "🧠"
requires:
env:
- OPENAI_API_KEY
plugins:
- memory-lancedb
---
# Elite Longterm Memory 🧠
**The ultimate memory system for AI agents.** Combines 6 proven approaches into one bulletproof architecture.
Never lose context. Never forget decisions. Never repeat mistakes.
## Architecture Overview
```
┌─────────────────────────────────────────────────────────────────┐
│ ELITE LONGTERM MEMORY │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ HOT RAM │ │ WARM STORE │ │ COLD STORE │ │
│ │ │ │ │ │ │ │
│ │ SESSION- │ │ LanceDB │ │ Git-Notes │ │
│ │ STATE.md │ │ Vectors │ │ Knowledge │ │
│ │ │ │ │ │ Graph │ │
│ │ (survives │ │ (semantic │ │ (permanent │ │
│ │ compaction)│ │ search) │ │ decisions) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │ │ │ │
│ └────────────────┼────────────────┘ │
│ ▼ │
│ ┌─────────────┐ │
│ │ MEMORY.md │ ← Curated long-term │
│ │ + daily/ │ (human-readable) │
│ └─────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ SuperMemory │ ← Cloud backup (optional) │
│ │ API │ │
│ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
```
## The 5 Memory Layers
### Layer 1: HOT RAM (SESSION-STATE.md)
**From: bulletproof-memory**
Active working memory that survives compaction. Write-Ahead Log protocol.
```markdown
# SESSION-STATE.md — Active Working Memory
## Current Task
[What we're working on RIGHT NOW]
## Key Context
- User preference: ...
- Decision made: ...
- Blocker: ...
## Pending Actions
- [ ] ...
```
**Rule:** Write BEFORE responding. Triggered by user input, not agent memory.
### Layer 2: WARM STORE (LanceDB Vectors)
**From: lancedb-memory**
Semantic search across all memories. Auto-recall injects relevant context.
```bash
# Auto-recall (happens automatically)
memory_recall query="project status" limit=5
# Manual store
memory_store text="User prefers dark mode" category="preference" importance=0.9
```
### Layer 3: COLD STORE (Git-Notes Knowledge Graph)
**From: git-notes-memory**
Structured decisions, learnings, and context. Branch-aware.
```bash
# Store a decision (SILENT - never announce)
python3 memory.py -p $DIR remember '{"type":"decision","content":"Use React for frontend"}' -t tech -i h
# Retrieve context
python3 memory.py -p $DIR get "frontend"
```
### Layer 4: CURATED ARCHIVE (MEMORY.md + daily/)
**From: OpenClaw native**
Human-readable long-term memory. Daily logs + distilled wisdom.
```
workspace/
├── MEMORY.md # Curated long-term (the good stuff)
└── memory/
├── 2026-01-30.md # Daily log
├── 2026-01-29.md
└── topics/ # Topic-specific files
```
### Layer 5: CLOUD BACKUP (SuperMemory) — Optional
**From: supermemory**
Cross-device sync. Chat with your knowledge base.
```bash
export SUPERMEMORY_API_KEY="your-key"
supermemory add "Important context"
supermemory search "what did we decide about..."
```
### Layer 6: AUTO-EXTRACTION (Mem0) — Recommended
**NEW: Automatic fact extraction**
Mem0 automatically extracts facts from conversations. 80% token reduction.
```bash
npm install mem0ai
export MEM0_API_KEY="your-key"
```
```javascript
const { MemoryClient } = require('mem0ai');
const client = new MemoryClient({ apiKey: process.env.MEM0_API_KEY });
// Conversations auto-extract facts
await client.add(messages, { user_id: "user123" });
// Retrieve relevant memories
const memories = await client.search(query, { user_id: "user123" });
```
Benefits:
- Auto-extracts preferences, decisions, facts
- Deduplicates and updates existing memories
- 80% reduction in tokens vs raw history
- Works across sessions automatically
## Quick Setup
### 1. Create SESSION-STATE.md (Hot RAM)
```bash
cat > SESSION-STATE.md << 'EOF'
# SESSION-STATE.md — Active Working Memory
This file is the agent's "RAM" — survives compaction, restarts, distractions.
## Current Task
[None]
## Key Context
[None yet]
## Pending Actions
- [ ] None
## Recent Decisions
[None yet]
---
*Last updated: [timestamp]*
EOF
```
### 2. Enable LanceDB (Warm Store)
In `~/.openclaw/openclaw.json`:
```json
{
"memorySearch": {
"enabled": true,
"provider": "openai",
"sources": ["memory"],
"minScore": 0.3,
"maxResults": 10
},
"plugins": {
"entries": {
"memory-lancedb": {
"enabled": true,
"config": {
"autoCapture": false,
"autoRecall": true,
"captureCategories": ["preference", "decision", "fact"],
"minImportance": 0.7
}
}
}
}
}
```
### 3. Initialize Git-Notes (Cold Store)
```bash
cd ~/clawd
git init # if not already
python3 skills/git-notes-memory/memory.py -p . sync --start
```
### 4. Verify MEMORY.md Structure
```bash
# Ensure you have:
# - MEMORY.md in workspace root
# - memory/ folder for daily logs
mkdir -p memory
```
### 5. (Optional) Setup SuperMemory
```bash
export SUPERMEMORY_API_KEY="your-key"
# Add to ~/.zshrc for persistence
```
## Agent Instructions
### On Session Start
1. Read SESSION-STATE.md — this is your hot context
2. Run `memory_search` for relevant prior context
3. Check memory/YYYY-MM-DD.md for recent activity
### During Conversation
1. **User gives concrete detail?** → Write to SESSION-STATE.md BEFORE responding
2. **Important decision made?** → Store in Git-Notes (SILENTLY)
3. **Preference expressed?** → `memory_store` with importance=0.9
### On Session End
1. Update SESSION-STATE.md with final state
2. Move significant items to MEMORY.md if worth keeping long-term
3. Create/update daily log in memory/YYYY-MM-DD.md
### Memory Hygiene (Weekly)
1. Review SESSION-STATE.md — archive completed tasks
2. Check LanceDB for junk: `memory_recall query="*" limit=50`
3. Clear irrelevant vectors: `memory_forget id=<id>`
4. Consolidate daily logs into MEMORY.md
## The WAL Protocol (Critical)
**Write-Ahead Log:** Write state BEFORE responding, not after.
| Trigger | Action |
|---------|--------|
| User states preference | Write to SESSION-STATE.md → then respond |
| User makes decision | Write to SESSION-STATE.md → then respond |
| User gives deadline | Write to SESSION-STATE.md → then respond |
| User corrects you | Write to SESSION-STATE.md → then respond |
**Why?** If you respond first and crash/compact before saving, context is lost. WAL ensures durability.
## Example Workflow
```
User: "Let's use Tailwind for this project, not vanilla CSS"
Agent (internal):
1. Write to SESSION-STATE.md: "Decision: Use Tailwind, not vanilla CSS"
2. Store in Git-Notes: decision about CSS framework
3. memory_store: "User prefers Tailwind over vanilla CSS" importance=0.9
4. THEN respond: "Got it — Tailwind it is..."
```
## Maintenance Commands
```bash
# Audit vector memory
memory_recall query="*" limit=50
# Clear all vectors (nuclear option)
rm -rf ~/.openclaw/memory/lancedb/
openclaw gateway restart
# Export Git-Notes
python3 memory.py -p . export --format json > memories.json
# Check memory health
du -sh ~/.openclaw/memory/
wc -l MEMORY.md
ls -la memory/
```
## Why Memory Fails
Understanding the root causes helps you fix them:
| Failure Mode | Cause | Fix |
|--------------|-------|-----|
| Forgets everything | `memory_search` disabled | Enable + add OpenAI key |
| Files not loaded | Agent skips reading memory | Add to AGENTS.md rules |
| Facts not captured | No auto-extraction | Use Mem0 or manual logging |
| Sub-agents isolated | Don't inherit context | Pass context in task prompt |
| Repeats mistakes | Lessons not logged | Write to memory/lessons.md |
## Solutions (Ranked by Effort)
### 1. Quick Win: Enable memory_search
If you have an OpenAI key, enable semantic search:
```bash
openclaw configure --section web
```
This enables vector search over MEMORY.md + memory/*.md files.
### 2. Recommended: Mem0 Integration
Auto-extract facts from conversations. 80% token reduction.
```bash
npm install mem0ai
```
```javascript
const { MemoryClient } = require('mem0ai');
const client = new MemoryClient({ apiKey: process.env.MEM0_API_KEY });
// Auto-extract and store
await client.add([
{ role: "user", content: "I prefer Tailwind over vanilla CSS" }
], { user_id: "ty" });
// Retrieve relevant memories
const memories = await client.search("CSS preferences", { user_id: "ty" });
```
### 3. Better File Structure (No Dependencies)
```
memory/
├── projects/
│ ├── strykr.md
│ └── taska.md
├── people/
│ └── contacts.md
├── decisions/
│ └── 2026-01.md
├── lessons/
│ └── mistakes.md
└── preferences.md
```
Keep MEMORY.md as a summary (<5KB), link to detailed files.
## Immediate Fixes Checklist
| Problem | Fix |
|---------|-----|
| Forgets preferences | Add `## Preferences` section to MEMORY.md |
| Repeats mistakes | Log every mistake to `memory/lessons.md` |
| Sub-agents lack context | Include key context in spawn task prompt |
| Forgets recent work | Strict daily file discipline |
| Memory search not working | Check `OPENAI_API_KEY` is set |
## Troubleshooting
**Agent keeps forgetting mid-conversation:**
→ SESSION-STATE.md not being updated. Check WAL protocol.
**Irrelevant memories injected:**
→ Disable autoCapture, increase minImportance threshold.
**Memory too large, slow recall:**
→ Run hygiene: clear old vectors, archive daily logs.
**Git-Notes not persisting:**
→ Run `git notes push` to sync with remote.
**memory_search returns nothing:**
→ Check OpenAI API key: `echo $OPENAI_API_KEY`
→ Verify memorySearch enabled in openclaw.json
---
## Links
- bulletproof-memory: https://clawdhub.com/skills/bulletproof-memory
- lancedb-memory: https://clawdhub.com/skills/lancedb-memory
- git-notes-memory: https://clawdhub.com/skills/git-notes-memory
- memory-hygiene: https://clawdhub.com/skills/memory-hygiene
- supermemory: https://clawdhub.com/skills/supermemory
---
*Built by [@NextXFrontier](https://x.com/NextXFrontier) — Part of the Next Frontier AI toolkit*
FILE:README.md
# Elite Longterm Memory 🧠
**The ultimate memory system for AI agents.** Never lose context again.
[](https://www.npmjs.com/package/elite-longterm-memory)
[](https://www.npmjs.com/package/elite-longterm-memory)
[](https://opensource.org/licenses/MIT)
---
## Works With
<p align="center">
<img src="https://img.shields.io/badge/Claude-AI-orange?style=for-the-badge&logo=anthropic" alt="Claude AI" />
<img src="https://img.shields.io/badge/GPT-OpenAI-412991?style=for-the-badge&logo=openai" alt="GPT" />
<img src="https://img.shields.io/badge/Cursor-IDE-000000?style=for-the-badge" alt="Cursor" />
<img src="https://img.shields.io/badge/LangChain-Framework-1C3C3C?style=for-the-badge" alt="LangChain" />
</p>
<p align="center">
<strong>Built for:</strong> Clawdbot • Moltbot • Claude Code • Any AI Agent
</p>
---
Combines 7 proven memory approaches into one bulletproof architecture:
- ✅ **Bulletproof WAL Protocol** — Write-ahead logging survives compaction
- ✅ **LanceDB Vector Search** — Semantic recall of relevant memories
- ✅ **Git-Notes Knowledge Graph** — Structured decisions, branch-aware
- ✅ **File-Based Archives** — Human-readable MEMORY.md + daily logs
- ✅ **Cloud Backup** — Optional SuperMemory sync
- ✅ **Memory Hygiene** — Keep vectors lean, prevent token waste
- ✅ **Mem0 Auto-Extraction** — Automatic fact extraction, 80% token reduction
## Quick Start
```bash
# Initialize in your workspace
npx elite-longterm-memory init
# Check status
npx elite-longterm-memory status
# Create today's log
npx elite-longterm-memory today
```
## Architecture
```
┌─────────────────────────────────────────────────────┐
│ ELITE LONGTERM MEMORY │
├─────────────────────────────────────────────────────┤
│ HOT RAM WARM STORE COLD STORE │
│ SESSION-STATE.md → LanceDB → Git-Notes │
│ (survives (semantic (permanent │
│ compaction) search) decisions) │
│ │ │ │ │
│ └──────────────┼────────────────┘ │
│ ▼ │
│ MEMORY.md │
│ (curated archive) │
└─────────────────────────────────────────────────────┘
```
## The 5 Memory Layers
| Layer | File/System | Purpose | Persistence |
|-------|-------------|---------|-------------|
| 1. Hot RAM | SESSION-STATE.md | Active task context | Survives compaction |
| 2. Warm Store | LanceDB | Semantic search | Auto-recall |
| 3. Cold Store | Git-Notes | Structured decisions | Permanent |
| 4. Archive | MEMORY.md + daily/ | Human-readable | Curated |
| 5. Cloud | SuperMemory | Cross-device sync | Optional |
## The WAL Protocol
**Critical insight:** Write state BEFORE responding, not after.
```
User: "Let's use Tailwind for this project"
Agent (internal):
1. Write to SESSION-STATE.md → "Decision: Use Tailwind"
2. THEN respond → "Got it — Tailwind it is..."
```
If you respond first and crash before saving, context is lost. WAL ensures durability.
## Why Memory Fails (And How to Fix It)
| Problem | Cause | Fix |
|---------|-------|-----|
| Forgets everything | memory_search disabled | Enable + add OpenAI key |
| Repeats mistakes | Lessons not logged | Write to memory/lessons.md |
| Sub-agents isolated | No context inheritance | Pass context in task prompt |
| Facts not captured | No auto-extraction | Use Mem0 (see below) |
## Mem0 Integration (Recommended)
Auto-extract facts from conversations. 80% token reduction.
```bash
npm install mem0ai
export MEM0_API_KEY="your-key"
```
```javascript
const { MemoryClient } = require('mem0ai');
const client = new MemoryClient({ apiKey: process.env.MEM0_API_KEY });
// Auto-extracts facts from messages
await client.add(messages, { user_id: "user123" });
// Retrieve relevant memories
const memories = await client.search(query, { user_id: "user123" });
```
## For Clawdbot/Moltbot Users
Add to `~/.clawdbot/clawdbot.json`:
```json
{
"memorySearch": {
"enabled": true,
"provider": "openai",
"sources": ["memory"]
}
}
```
## Files Created
```
workspace/
├── SESSION-STATE.md # Hot RAM (active context)
├── MEMORY.md # Curated long-term memory
└── memory/
├── 2026-01-30.md # Daily logs
└── ...
```
## Commands
```bash
elite-memory init # Initialize memory system
elite-memory status # Check health
elite-memory today # Create today's log
elite-memory help # Show help
```
## Links
- [Full Documentation (SKILL.md)](./SKILL.md)
- [ClawdHub](https://clawdhub.com/skills/elite-longterm-memory)
- [GitHub](https://github.com/NextFrontierBuilds/elite-longterm-memory)
---
Built by [@NextXFrontier](https://x.com/NextXFrontier)
FILE:_meta.json
{
"ownerId": "kn7ctkm6wpdn2rrdpbndrp7d2s81w3hb",
"slug": "lovefromio-elite-longterm-memory",
"version": "1.2.4",
"publishedAt": 1776659812452
}
FILE:bin/elite-memory.js
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const TEMPLATES = {
'session-state': `# SESSION-STATE.md — Active Working Memory
This file is the agent's "RAM" — survives compaction, restarts, distractions.
Chat history is a BUFFER. This file is STORAGE.
## Current Task
[None]
## Key Context
[None yet]
## Pending Actions
- [ ] None
## Recent Decisions
[None yet]
---
*Last updated: new Date().toISOString()*
`,
'memory-md': `# MEMORY.md — Long-Term Memory
## About the User
[Add user preferences, communication style, etc.]
## Projects
[Active projects and their status]
## Decisions Log
[Important decisions and why they were made]
## Lessons Learned
[Mistakes to avoid, patterns that work]
## Preferences
[Tools, frameworks, workflows the user prefers]
---
*Curated memory — distill insights from daily logs here*
`,
'daily-template': `# {{DATE}} — Daily Log
## Tasks Completed
-
## Decisions Made
-
## Lessons Learned
-
## Tomorrow
-
`
};
const commands = {
init: () => {
console.log('🧠 Initializing Elite Longterm Memory...\n');
// Create SESSION-STATE.md
if (!fs.existsSync('SESSION-STATE.md')) {
fs.writeFileSync('SESSION-STATE.md', TEMPLATES['session-state']);
console.log('✓ Created SESSION-STATE.md (Hot RAM)');
} else {
console.log('• SESSION-STATE.md already exists');
}
// Create MEMORY.md
if (!fs.existsSync('MEMORY.md')) {
fs.writeFileSync('MEMORY.md', TEMPLATES['memory-md']);
console.log('✓ Created MEMORY.md (Curated Archive)');
} else {
console.log('• MEMORY.md already exists');
}
// Create memory directory
if (!fs.existsSync('memory')) {
fs.mkdirSync('memory', { recursive: true });
console.log('✓ Created memory/ directory');
} else {
console.log('• memory/ directory already exists');
}
// Create today's log
const today = new Date().toISOString().split('T')[0];
const todayFile = `memory/today.md`;
if (!fs.existsSync(todayFile)) {
const content = TEMPLATES['daily-template'].replace('{{DATE}}', today);
fs.writeFileSync(todayFile, content);
console.log(`✓ Created todayFile`);
}
console.log('\n🎉 Elite Longterm Memory initialized!');
console.log('\nNext steps:');
console.log('1. Add SESSION-STATE.md to your agent context');
console.log('2. Configure LanceDB plugin in clawdbot.json');
console.log('3. Review SKILL.md for full setup guide');
},
today: () => {
const today = new Date().toISOString().split('T')[0];
const todayFile = `memory/today.md`;
if (!fs.existsSync('memory')) {
fs.mkdirSync('memory', { recursive: true });
}
if (!fs.existsSync(todayFile)) {
const content = TEMPLATES['daily-template'].replace('{{DATE}}', today);
fs.writeFileSync(todayFile, content);
console.log(`✓ Created todayFile`);
} else {
console.log(`• todayFile already exists`);
}
},
status: () => {
console.log('🧠 Elite Longterm Memory Status\n');
// Check SESSION-STATE.md
if (fs.existsSync('SESSION-STATE.md')) {
const stat = fs.statSync('SESSION-STATE.md');
console.log(`✓ SESSION-STATE.md ((stat.size / 1024).toFixed(1)KB, modified stat.mtime.toLocaleString())`);
} else {
console.log('✗ SESSION-STATE.md missing');
}
// Check MEMORY.md
if (fs.existsSync('MEMORY.md')) {
const stat = fs.statSync('MEMORY.md');
const lines = fs.readFileSync('MEMORY.md', 'utf8').split('\n').length;
console.log(`✓ MEMORY.md (lines lines, (stat.size / 1024).toFixed(1)KB)`);
} else {
console.log('✗ MEMORY.md missing');
}
// Check memory directory
if (fs.existsSync('memory')) {
const files = fs.readdirSync('memory').filter(f => f.endsWith('.md'));
console.log(`✓ memory/ (files.length daily logs)`);
} else {
console.log('✗ memory/ directory missing');
}
// Check LanceDB
const lancedbPath = path.join(process.env.HOME, '.clawdbot/memory/lancedb');
if (fs.existsSync(lancedbPath)) {
console.log('✓ LanceDB vectors initialized');
} else {
console.log('• LanceDB not initialized (optional)');
}
},
help: () => {
console.log(`
🧠 Elite Longterm Memory CLI
Commands:
init Initialize memory system in current directory
today Create today's daily log file
status Check memory system health
help Show this help
Usage:
npx elite-longterm-memory init
npx elite-longterm-memory status
`);
}
};
const command = process.argv[2] || 'help';
if (commands[command]) {
commands[command]();
} else {
console.log(`Unknown command: command`);
commands.help();
}
FILE:package.json
{
"name": "elite-longterm-memory",
"version": "1.2.2",
"description": "Ultimate AI agent memory system. Combines bulletproof WAL protocol, vector search, git-based knowledge graphs, cloud backup, and maintenance hygiene. Never lose context again.",
"keywords": [
"memory",
"ai-agent",
"long-term-memory",
"vector-search",
"lancedb",
"git-notes",
"wal",
"persistent-context",
"claude",
"gpt",
"openclaw",
"moltbot",
"openclaw",
"cursor",
"copilot",
"ai",
"llm",
"automation",
"context-management",
"mem0",
"auto-extraction",
"fact-extraction"
],
"optionalDependencies": {
"mem0ai": "^1.0.0"
},
"author": "NextFrontierBuilds",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/NextFrontierBuilds/elite-longterm-memory"
},
"homepage": "https://github.com/NextFrontierBuilds/elite-longterm-memory",
"bugs": {
"url": "https://github.com/NextFrontierBuilds/elite-longterm-memory/issues"
},
"bin": {
"elite-memory": "./bin/elite-memory.js"
},
"files": [
"SKILL.md",
"bin/",
"templates/",
"README.md"
]
}
Automatically extract patterns, best practices, and reusable knowledge from construction automation sessions to improve future performance.
---
slug: "continuous-learning"
display_name: "Continuous Learning Construction"
description: "Automatically extract patterns, best practices, and reusable knowledge from construction automation sessions to improve future performance."
---
# Continuous Learning for Construction Automation
This skill enables automatic extraction of valuable patterns, solutions, and best practices from construction automation sessions to build institutional knowledge.
## When to Use
Activate this skill:
- At the end of complex estimation sessions
- After solving non-trivial data processing problems
- When discovering new integration patterns
- After completing successful document processing
- When developing new automation workflows
## Pattern Extraction Framework
### 1. Session Analysis
```python
class ConstructionSessionAnalyzer:
"""Extract learnings from automation sessions"""
# Categories of learnable patterns
PATTERN_CATEGORIES = [
'data_processing', # Data transformation patterns
'estimation', # Cost estimation techniques
'scheduling', # Schedule optimization patterns
'integration', # API/system integration patterns
'document_processing', # Document handling patterns
'quality_assurance', # Validation and QA patterns
'error_handling', # Error resolution patterns
'optimization' # Performance optimization patterns
]
def analyze_session(self, session_log: list) -> dict:
"""Extract patterns from session history"""
patterns = {
'successful_solutions': [],
'error_resolutions': [],
'optimization_discoveries': [],
'integration_patterns': [],
'reusable_code': [],
'decision_rationales': []
}
for entry in session_log:
if self._is_solution(entry):
patterns['successful_solutions'].append(
self._extract_solution_pattern(entry)
)
if self._is_error_resolution(entry):
patterns['error_resolutions'].append(
self._extract_error_pattern(entry)
)
if self._is_optimization(entry):
patterns['optimization_discoveries'].append(
self._extract_optimization(entry)
)
return patterns
```
### 2. Knowledge Categories for Construction
#### 2.1 Cost Estimation Patterns
```yaml
# Example learned pattern
pattern:
name: "electrical_cost_adjustment_pattern"
category: "estimation"
context: "When estimating electrical work for high-rise buildings"
problem: "Standard rates don't account for vertical transportation costs"
solution: |
Apply height factor multiplier:
- Floors 1-5: 1.0x base rate
- Floors 6-15: 1.15x base rate
- Floors 16-30: 1.25x base rate
- Floors 30+: 1.35x base rate
confidence: 0.85
source_sessions: ["session_2026_01_15", "session_2026_01_20"]
validations: 3
```
#### 2.2 BIM Data Processing Patterns
```yaml
pattern:
name: "revit_level_extraction"
category: "data_processing"
context: "Extracting elements by level from Revit exports"
problem: "Elements sometimes missing level association"
solution: |
1. First check 'Level' parameter
2. If missing, check 'Reference Level' parameter
3. If still missing, derive from bounding box Z coordinate
4. Map Z ranges to known level elevations
code_snippet: |
def get_element_level(element: dict, levels: list) -> str:
# Direct level parameter
if level := element.get('Level'):
return level
# Reference level fallback
if ref_level := element.get('Reference Level'):
return ref_level
# Derive from geometry
z_coord = element['BoundingBox']['Min']['Z']
return find_nearest_level(z_coord, levels)
confidence: 0.92
```
#### 2.3 Integration Patterns
```yaml
pattern:
name: "procore_rate_limit_handling"
category: "integration"
context: "Syncing data with Procore API"
problem: "API returns 429 Too Many Requests during bulk operations"
solution: |
Implement exponential backoff with jitter:
1. Initial delay: 1 second
2. Multiply by 2 on each retry
3. Add random jitter (0-500ms)
4. Max retries: 5
5. Max delay: 32 seconds
code_snippet: |
async def procore_request_with_retry(url, data):
delay = 1
for attempt in range(5):
try:
response = await procore_api.post(url, data)
return response
except RateLimitError:
jitter = random.uniform(0, 0.5)
await asyncio.sleep(delay + jitter)
delay *= 2
raise MaxRetriesExceeded()
confidence: 0.95
```
#### 2.4 Error Resolution Patterns
```yaml
pattern:
name: "cwicr_no_match_resolution"
category: "error_handling"
context: "CWICR semantic search returns no relevant matches"
problem: "Query too specific or uses non-standard terminology"
solution: |
Resolution steps:
1. Simplify query to core concepts
2. Remove brand names and specifications
3. Try alternative terminology (US vs UK terms)
4. Expand search to parent category
5. If still no match, flag for manual mapping
examples:
- original: "Kohler K-4519 wall-mounted water closet"
simplified: "wall mounted toilet"
- original: "Lutron Caseta wireless dimmer switch"
simplified: "dimmer switch"
confidence: 0.88
```
### 3. Learning Pipeline
```python
class ConstructionLearningPipeline:
"""Continuous learning pipeline for construction automation"""
def __init__(self, knowledge_base_path: str):
self.kb_path = knowledge_base_path
self.patterns = self._load_patterns()
def learn_from_session(self, session: dict) -> list:
"""Extract and store learnings from session"""
# Analyze session
analyzer = ConstructionSessionAnalyzer()
new_patterns = analyzer.analyze_session(session['log'])
# Validate patterns
validated = []
for pattern in new_patterns['successful_solutions']:
if self._validate_pattern(pattern):
# Check if similar pattern exists
existing = self._find_similar_pattern(pattern)
if existing:
# Reinforce existing pattern
self._reinforce_pattern(existing, pattern)
else:
# Add new pattern
self._add_pattern(pattern)
validated.append(pattern)
# Persist to knowledge base
self._save_patterns()
return validated
def apply_learnings(self, context: dict) -> list:
"""Retrieve relevant patterns for current context"""
relevant_patterns = []
for pattern in self.patterns:
similarity = self._calculate_similarity(pattern['context'], context)
if similarity > 0.7:
relevant_patterns.append({
'pattern': pattern,
'relevance': similarity
})
return sorted(relevant_patterns, key=lambda x: x['relevance'], reverse=True)
def _validate_pattern(self, pattern: dict) -> bool:
"""Validate pattern before adding to knowledge base"""
# Check minimum confidence
if pattern.get('confidence', 0) < 0.6:
return False
# Check for code quality (if code snippet)
if code := pattern.get('code_snippet'):
if not self._is_valid_code(code):
return False
# Check for completeness
required_fields = ['name', 'category', 'context', 'solution']
if not all(f in pattern for f in required_fields):
return False
return True
```
### 4. Knowledge Base Structure
```
knowledge_base/
├── patterns/
│ ├── estimation/
│ │ ├── height_factors.yaml
│ │ ├── material_adjustments.yaml
│ │ └── labor_productivity.yaml
│ ├── data_processing/
│ │ ├── revit_extraction.yaml
│ │ ├── ifc_parsing.yaml
│ │ └── excel_transformations.yaml
│ ├── integration/
│ │ ├── procore_patterns.yaml
│ │ ├── plangrid_patterns.yaml
│ │ └── webhook_handlers.yaml
│ └── error_handling/
│ ├── cwicr_resolutions.yaml
│ ├── api_errors.yaml
│ └── data_validation.yaml
├── code_snippets/
│ ├── python/
│ ├── javascript/
│ └── sql/
├── decision_trees/
│ ├── estimate_type_selection.yaml
│ ├── schedule_method_selection.yaml
│ └── integration_approach.yaml
└── metrics/
├── pattern_usage.json
└── success_rates.json
```
### 5. Session End Learning Prompt
At the end of each construction automation session:
```markdown
## Session Learning Review
### What Worked Well
- [Successful approaches discovered]
- [Efficient patterns used]
- [Integrations that worked smoothly]
### Challenges Overcome
- [Errors encountered and how resolved]
- [Workarounds developed]
- [Edge cases handled]
### New Patterns Discovered
- [Novel approaches to problems]
- [Optimization techniques found]
- [Reusable code created]
### Knowledge to Preserve
- [Key learnings to remember]
- [Context-specific solutions]
- [Client/project-specific adaptations]
### Recommendations for Future
- [Improvements to suggest]
- [Patterns to apply elsewhere]
- [Automation opportunities identified]
```
### 6. Pattern Application
When starting new construction tasks:
```python
def suggest_approaches(task_context: dict) -> list:
"""Suggest learned approaches for new tasks"""
pipeline = ConstructionLearningPipeline('knowledge_base/')
relevant = pipeline.apply_learnings(task_context)
suggestions = []
for item in relevant[:5]: # Top 5 suggestions
pattern = item['pattern']
suggestions.append({
'name': pattern['name'],
'relevance': f"{item['relevance']*100:.0f}%",
'summary': pattern['solution'][:200],
'confidence': pattern['confidence'],
'previous_uses': pattern.get('usage_count', 0)
})
return suggestions
```
## Integration with Other Skills
This skill works with:
- **verification-loop-construction**: Learn from verification failures
- **security-review-construction**: Capture security patterns
- **estimation skills**: Build estimation knowledge base
- **integration skills**: Capture API patterns
## Usage Commands
```bash
# Extract learnings from current session
/learn
# View patterns for current context
/suggest-patterns
# Add manual pattern
/add-pattern --category estimation --name "my_pattern"
# Export knowledge base
/export-kb --format yaml
```
---
**Every session is an opportunity to learn. Capture knowledge to compound expertise over time.**
FILE:_meta.json
{
"ownerId": "kn7ctkm6wpdn2rrdpbndrp7d2s81w3hb",
"slug": "lovefromio-continuous-learning",
"version": "1.0.1",
"publishedAt": 1776659639782
}