diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a72a683fc..9012e6c11 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,10 +5,9 @@ on: branches: [main] pull_request: branches: [main] - workflow_dispatch: jobs: - unit: + test: runs-on: ubuntu-latest steps: @@ -23,203 +22,8 @@ jobs: - name: Install dependencies run: npm ci - - name: Lint - run: npm run lint - - - name: Format check - run: npm run format:check - - name: Type check run: npm run typecheck - name: Run tests run: npm test - - e2e: - runs-on: ubuntu-latest - timeout-minutes: 20 - permissions: - contents: write - pull-requests: write - - strategy: - fail-fast: false - matrix: - config: - - name: base - env: {} - - name: telegram - env: - TELEGRAM_BOT_TOKEN: "fake-telegram-bot-token-for-e2e" - TELEGRAM_DM_POLICY: "pairing" - - name: discord - env: - DISCORD_BOT_TOKEN: "fake-discord-bot-token-for-e2e" - DISCORD_DM_POLICY: "pairing" - - name: workers-ai - env: - CF_AI_GATEWAY_MODEL: "workers-ai/@cf/openai/gpt-oss-120b" - - name: e2e (${{ matrix.config.name }}) - - steps: - - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 22 - cache: npm - - - name: Install dependencies - run: npm ci - - - name: Install Terraform - uses: hashicorp/setup-terraform@v3 - with: - terraform_wrapper: false - - - name: Install system dependencies - run: sudo apt-get update -qq && sudo apt-get install -y -qq ffmpeg imagemagick bc - - - name: Install cctr - uses: taiki-e/install-action@v2 - with: - tool: cctr - - - name: Install plwr - uses: taiki-e/install-action@v2 - with: - tool: plwr@0.7.2 - - - name: Install Playwright browsers - run: npm install -g playwright && npx playwright install --with-deps chromium - - - name: Run E2E tests (${{ matrix.config.name }}) - id: e2e - continue-on-error: true - env: - # Cloud infrastructure credentials (from repo secrets with E2E_ prefix) - CLOUDFLARE_API_TOKEN: ${{ secrets.E2E_CLOUDFLARE_API_TOKEN }} - CF_ACCOUNT_ID: ${{ secrets.E2E_CF_ACCOUNT_ID }} - WORKERS_SUBDOMAIN: ${{ secrets.E2E_WORKERS_SUBDOMAIN }} - CF_ACCESS_TEAM_DOMAIN: ${{ secrets.E2E_CF_ACCESS_TEAM_DOMAIN }} - R2_ACCESS_KEY_ID: ${{ secrets.E2E_R2_ACCESS_KEY_ID }} - R2_SECRET_ACCESS_KEY: ${{ secrets.E2E_R2_SECRET_ACCESS_KEY }} - # AI provider โ Cloudflare AI Gateway (preferred) - CLOUDFLARE_AI_GATEWAY_API_KEY: ${{ secrets.CLOUDFLARE_AI_GATEWAY_API_KEY }} - CF_AI_GATEWAY_ACCOUNT_ID: ${{ secrets.CF_AI_GATEWAY_ACCOUNT_ID }} - CF_AI_GATEWAY_GATEWAY_ID: ${{ secrets.CF_AI_GATEWAY_GATEWAY_ID }} - # AI provider โ legacy (still supported) - AI_GATEWAY_API_KEY: ${{ secrets.AI_GATEWAY_API_KEY }} - AI_GATEWAY_BASE_URL: ${{ secrets.AI_GATEWAY_BASE_URL }} - # Unique test run ID for parallel isolation - E2E_TEST_RUN_ID: ${{ github.run_id }}-${{ github.run_attempt }}-${{ matrix.config.name }} - # Matrix-specific config - TELEGRAM_BOT_TOKEN: ${{ matrix.config.env.TELEGRAM_BOT_TOKEN }} - TELEGRAM_DM_POLICY: ${{ matrix.config.env.TELEGRAM_DM_POLICY }} - DISCORD_BOT_TOKEN: ${{ matrix.config.env.DISCORD_BOT_TOKEN }} - DISCORD_DM_POLICY: ${{ matrix.config.env.DISCORD_DM_POLICY }} - CF_AI_GATEWAY_MODEL: ${{ matrix.config.env.CF_AI_GATEWAY_MODEL }} - run: cctr -vv test/e2e - - - name: Generate video thumbnail - id: video - if: always() - run: | - if ls /tmp/moltworker-e2e-videos/*.mp4 1>/dev/null 2>&1; then - for mp4 in /tmp/moltworker-e2e-videos/*.mp4; do - thumb="${mp4%.mp4}.png" - - # Extract middle frame as thumbnail - duration=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$mp4") - midpoint=$(echo "$duration / 2" | bc -l) - ffmpeg -y -ss "$midpoint" -i "$mp4" -vframes 1 -update 1 -q:v 2 "$thumb" - - # Add play button overlay - width=$(identify -format '%w' "$thumb") - height=$(identify -format '%h' "$thumb") - cx=$((width / 2)) - cy=$((height / 2)) - convert "$thumb" \ - -fill 'rgba(0,0,0,0.6)' -draw "circle ${cx},${cy} $((cx+50)),${cy}" \ - -fill 'white' -draw "polygon $((cx-15)),$((cy-25)) $((cx-15)),$((cy+25)) $((cx+30)),${cy}" \ - "$thumb" - - echo "video_path=$mp4" >> $GITHUB_OUTPUT - echo "video_name=$(basename $mp4)" >> $GITHUB_OUTPUT - echo "thumb_path=$thumb" >> $GITHUB_OUTPUT - echo "thumb_name=$(basename $thumb)" >> $GITHUB_OUTPUT - done - echo "has_video=true" >> $GITHUB_OUTPUT - else - echo "has_video=false" >> $GITHUB_OUTPUT - fi - - - name: Prepare video for upload - id: prepare - if: always() && steps.video.outputs.has_video == 'true' - run: | - mkdir -p /tmp/e2e-video-upload/videos/${{ github.run_id }}-${{ matrix.config.name }} - cp "${{ steps.video.outputs.video_path }}" /tmp/e2e-video-upload/videos/${{ github.run_id }}-${{ matrix.config.name }}/ - cp "${{ steps.video.outputs.thumb_path }}" /tmp/e2e-video-upload/videos/${{ github.run_id }}-${{ matrix.config.name }}/ - echo "video_url=https://github.com/${{ github.repository }}/raw/e2e-artifacts-${{ matrix.config.name }}/videos/${{ github.run_id }}-${{ matrix.config.name }}/${{ steps.video.outputs.video_name }}" >> $GITHUB_OUTPUT - echo "thumb_url=https://github.com/${{ github.repository }}/raw/e2e-artifacts-${{ matrix.config.name }}/videos/${{ github.run_id }}-${{ matrix.config.name }}/${{ steps.video.outputs.thumb_name }}" >> $GITHUB_OUTPUT - - - name: Upload video to e2e-artifacts branch - if: always() && steps.video.outputs.has_video == 'true' - uses: peaceiris/actions-gh-pages@v4 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: /tmp/e2e-video-upload - publish_branch: e2e-artifacts-${{ matrix.config.name }} - keep_files: true - - - name: Delete old video comments - if: always() && github.event_name == 'pull_request' - uses: actions/github-script@v7 - with: - script: | - const marker = ''; - const { data: comments } = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - }); - for (const comment of comments) { - if (comment.body.includes(marker)) { - await github.rest.issues.deleteComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: comment.id, - }); - } - } - - - name: Comment on PR with video - if: always() && github.event_name == 'pull_request' && steps.prepare.outputs.video_url - uses: peter-evans/create-or-update-comment@v4 - with: - issue-number: ${{ github.event.pull_request.number }} - body: | - - ## E2E Test Recording (${{ matrix.config.name }}) - - ${{ steps.e2e.outcome == 'success' && 'โ Tests passed' || 'โ Tests failed' }} - - [](${{ steps.prepare.outputs.video_url }}) - - - name: Add video link to summary - if: always() - run: | - echo "## E2E Test Recording" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - if [ "${{ steps.video.outputs.has_video }}" == "true" ]; then - echo "๐น [Download video](${{ steps.prepare.outputs.video_url }})" >> $GITHUB_STEP_SUMMARY - else - echo "โ ๏ธ No video recording found" >> $GITHUB_STEP_SUMMARY - fi - - - name: Fail if E2E tests failed - if: steps.e2e.outcome == 'failure' - run: exit 1 diff --git a/.gitignore b/.gitignore index 47777a5f1..215d6f0c0 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,16 @@ Thumbs.db # Docker build artifacts *.tar +# Local Claude settings +.claude/ + +# Clawdbot runtime config (contains tokens) +clawdbot/ +.clawdhub/ + +# Custom skills (user-specific) +skills/prompt-guard/ + # Veta agent memory .veta/ @@ -60,4 +70,4 @@ test/e2e/.dev.vars .wrangler-e2e-*.jsonc # npm config -.npmrc \ No newline at end of file +.npmrc diff --git a/AGENT_COMMS_SETUP.md b/AGENT_COMMS_SETUP.md new file mode 100644 index 000000000..524fde964 --- /dev/null +++ b/AGENT_COMMS_SETUP.md @@ -0,0 +1,204 @@ +# Agent Communication System - Setup Guide + +This guide will help you deploy and configure the inter-agent communication system. + +## Overview + +The system allows multiple AI agents (like `jihwan_cat` and `jino`) to communicate with each other via: +- **Layer 1**: JSONL file-based messaging (bypasses Telegram bot-to-bot restrictions) +- **Layer 2**: Automatic mirroring to Telegram group (so you can observe and intervene) + +## Deployment Steps + +### 1. Set Environment Variable (Optional but Recommended) + +If you want messages mirrored to Telegram, set the group chat ID: + +```bash +cd "/Users/mac/Dropbox/๋ด Mac (MacBook-Air.local)/Downloads/moltworker" + +# Option A: Use your existing owner ID (messages go to DM) +# Already set if you have TELEGRAM_OWNER_ID + +# Option B: Create a group chat and use that ID +# 1. Create a Telegram group with your bot +# 2. Get the chat ID (it will be negative, like -1001234567890) +# 3. Set the secret: +echo "-1001234567890" | npx wrangler secret put TELEGRAM_AGENT_GROUP_ID --name moltbot-sandbox +``` + +### 2. Deploy the Worker + +```bash +cd "/Users/mac/Dropbox/๋ด Mac (MacBook-Air.local)/Downloads/moltworker" +npm run deploy +``` + +This will: +- Build and deploy the worker +- Upload all scripts including `scripts/agent-comms/*` +- The container will start with the new `start-openclaw.sh` + +### 3. Wait for Container to Start + +The container takes about 60-90 seconds to fully initialize. You can check status: + +```bash +curl -s "https://moltbot-sandbox.astin-43b.workers.dev/debug/processes" +``` + +Look for `openclaw gateway` in the running processes. + +### 4. Verify Setup + +Run the setup verification script via the debug CLI: + +```bash +curl -s "https://moltbot-sandbox.astin-43b.workers.dev/debug/cli?cmd=$(echo 'node /root/clawd/moltworker/scripts/agent-comms/setup-agents.js' | jq -sRr @uri)" +``` + +This will check: +- โ All scripts are present +- โ TOOLS.md is accessible +- โ Message bus is initialized +- โ Environment variables are set + +### 5. Test the System + +Run the test script: + +```bash +curl -s "https://moltbot-sandbox.astin-43b.workers.dev/debug/cli?cmd=$(echo 'bash /root/clawd/moltworker/scripts/agent-comms/test-system.sh' | jq -sRr @uri)" +``` + +This will: +- Send 3 test messages +- Show messages in the bus +- Test the Telegram mirroring (if configured) + +### 6. Restart Gateway (to Pick Up Changes) + +```bash +curl -s -X POST "https://moltbot-sandbox.astin-43b.workers.dev/api/admin/gateway/restart" +``` + +Wait ~60s for the gateway to restart, then the message watcher will start automatically. + +## Using the System + +### For Your Agents + +Agents can send messages using the `exec` tool in OpenClaw: + +**Example prompt to jihwan_cat:** +``` +Send a message to jino asking them to help with data analysis: +exec: node /root/clawd/moltworker/scripts/agent-comms/send-message.js --from jihwan_cat --to jino --message "Can you help analyze the latest metrics?" +``` + +The message will: +1. Be written to `/root/clawd/agent-messages.jsonl` +2. Within 30 seconds, appear in your Telegram group/chat as: + ``` + [jihwan_cat โ jino] 02/19 15:30 + Can you help analyze the latest metrics? + ``` + +### For You (Human) + +- **Observe**: All agent-to-agent messages appear in Telegram +- **Intervene**: Reply in the group or send commands directly to agents +- **Monitor**: Check message bus file via debug CLI if needed + +## Troubleshooting + +### Messages Not Appearing in Telegram + +**Check if watcher is running:** +```bash +curl -s "https://moltbot-sandbox.astin-43b.workers.dev/debug/cli?cmd=$(echo 'ps aux | grep watch-messages' | jq -sRr @uri)" +``` + +**Check watcher logs:** +```bash +curl -s "https://moltbot-sandbox.astin-43b.workers.dev/debug/cli?cmd=$(echo 'tail -20 /tmp/r2-sync.log' | jq -sRr @uri)" +``` + +**Manually run watcher:** +```bash +curl -s "https://moltbot-sandbox.astin-43b.workers.dev/debug/cli?cmd=$(echo 'node /root/clawd/moltworker/scripts/agent-comms/watch-messages.js' | jq -sRr @uri)" +``` + +### Check Message Bus File + +```bash +curl -s "https://moltbot-sandbox.astin-43b.workers.dev/debug/cli?cmd=$(echo 'cat /root/clawd/agent-messages.jsonl | tail -10' | jq -sRr @uri)" +``` + +### Check if TOOLS.md is Loaded + +Agents should have TOOLS.md in their context. Verify: + +```bash +curl -s "https://moltbot-sandbox.astin-43b.workers.dev/debug/cli?cmd=$(echo 'ls -la /root/clawd/ | grep TOOLS' | jq -sRr @uri)" +``` + +### Force Restart Background Services + +```bash +# Restart the entire gateway (this restarts all background loops) +curl -s -X POST "https://moltbot-sandbox.astin-43b.workers.dev/api/admin/gateway/restart" +``` + +## Advanced Usage + +### Broadcast Messages + +Send to all agents: +```bash +node /root/clawd/moltworker/scripts/agent-comms/send-message.js \ + --from jihwan_cat \ + --to all \ + --message "Announcement: maintenance window at 3pm" +``` + +### Read Messages Programmatically + +From an agent or script: +```javascript +const { readNewMessages, markAsRead } = require('/root/clawd/moltworker/scripts/agent-comms/message-bus'); + +// Get messages for jino +const messages = readNewMessages('jino'); +messages.forEach(msg => { + console.log(`From ${msg.from}: ${msg.message}`); +}); + +// Mark as read +if (messages.length > 0) { + markAsRead('jino', messages[messages.length - 1].id); +} +``` + +### Inspect Message History + +```bash +# Last 20 messages +curl -s "https://moltbot-sandbox.astin-43b.workers.dev/debug/cli?cmd=$(echo 'tail -20 /root/clawd/agent-messages.jsonl' | jq -sRr @uri)" + +# Count total messages +curl -s "https://moltbot-sandbox.astin-43b.workers.dev/debug/cli?cmd=$(echo 'wc -l /root/clawd/agent-messages.jsonl' | jq -sRr @uri)" +``` + +## Architecture Details + +See `scripts/agent-comms/README.md` for detailed architecture documentation. + +## Next Steps + +1. **Configure agents**: Update each agent's identity/personality to know about other agents +2. **Define workflows**: Decide which agent handles which types of tasks +3. **Monitor interactions**: Watch the Telegram group to see how agents coordinate +4. **Iterate**: Adjust agent prompts based on how they communicate + +Enjoy your multi-agent system! ๐คโจ diff --git a/DEPLOYMENT_SUMMARY.md b/DEPLOYMENT_SUMMARY.md new file mode 100644 index 000000000..b63128dfb --- /dev/null +++ b/DEPLOYMENT_SUMMARY.md @@ -0,0 +1,118 @@ +# Agent Communication System - Deployment Summary + +## What Was Built + +A two-layer inter-agent communication system that allows `jihwan_cat` and `jino` to communicate via: + +### Layer 1: JSONL Message Bus +- File-based messaging at `/root/clawd/agent-messages.jsonl` +- Bypasses Telegram's bot-to-bot restriction +- Persistent across sessions + +### Layer 2: Telegram Mirroring +- Background watcher runs every 30s +- Mirrors all agent messages to Telegram group +- Human can observe and intervene + +## Files Created/Modified + +### New Files +``` +scripts/ +โโโ agent-comms/ + โโโ README.md # Architecture documentation + โโโ message-bus.js # Core library + โโโ send-message.js # CLI to send messages + โโโ watch-messages.js # Telegram mirroring daemon + โโโ setup-agents.js # Setup verification script + โโโ test-system.sh # Testing script + +TOOLS.md # Agent documentation (auto-loaded by OpenClaw) +AGENT_COMMS_SETUP.md # Deployment guide (this file) +DEPLOYMENT_SUMMARY.md # This summary +``` + +### Modified Files +``` +Dockerfile # Added COPY for scripts/ and TOOLS.md +start-openclaw.sh # Added message watcher background loop +``` + +## Deployment Checklist + +- [ ] Commit changes to git +- [ ] Deploy via `npm run deploy` (builds Docker image and deploys to Cloudflare) +- [ ] Wait 60-90s for container to start +- [ ] (Optional) Set `TELEGRAM_AGENT_GROUP_ID` secret for group mirroring +- [ ] Verify setup via debug CLI +- [ ] Test with sample messages +- [ ] Restart gateway to activate watcher + +## Quick Start Commands + +### Deploy +```bash +cd "/Users/mac/Dropbox/๋ด Mac (MacBook-Air.local)/Downloads/moltworker" +npm run deploy +``` + +### Verify Setup +```bash +curl -s "https://moltbot-sandbox.astin-43b.workers.dev/debug/cli?cmd=$(echo 'node /root/clawd/moltworker/scripts/agent-comms/setup-agents.js' | jq -sRr @uri)" +``` + +### Test System +```bash +curl -s "https://moltbot-sandbox.astin-43b.workers.dev/debug/cli?cmd=$(echo 'bash /root/clawd/moltworker/scripts/agent-comms/test-system.sh' | jq -sRr @uri)" +``` + +### Restart Gateway +```bash +curl -s -X POST "https://moltbot-sandbox.astin-43b.workers.dev/api/admin/gateway/restart" +``` + +## Usage for Agents + +Agents use the `exec` tool to send messages: + +``` +node /root/clawd/moltworker/scripts/agent-comms/send-message.js \ + --from jihwan_cat \ + --to jino \ + --message "Can you help with this task?" +``` + +Messages appear in Telegram group within 30 seconds as: +``` +[jihwan_cat โ jino] 02/19 15:30 +Can you help with this task? +``` + +## Environment Variables + +| Variable | Required | Purpose | +|----------|----------|---------| +| `TELEGRAM_AGENT_GROUP_ID` | Optional | Chat ID for message mirroring (defaults to `TELEGRAM_OWNER_ID`) | + +## Next Steps After Deployment + +1. **Test the system** with the test script +2. **Update agent identities** to know about each other +3. **Define agent roles** (dev, writing, finance, etc.) +4. **Monitor interactions** in Telegram group +5. **Scale to more agents** as needed + +## Architecture Benefits + +โ **Bypasses Telegram bot-to-bot restriction** - Uses file-based communication +โ **Observable** - All messages visible in Telegram +โ **Persistent** - Messages survive restarts +โ **Simple** - Just JSONL append operations +โ **Scalable** - Can add more agents easily +โ **Intervenable** - Human can jump in anytime + +## References + +- Full setup guide: `AGENT_COMMS_SETUP.md` +- Architecture details: `scripts/agent-comms/README.md` +- Agent documentation: `TOOLS.md` (auto-loaded into agent context) diff --git a/Dockerfile b/Dockerfile index 996fe9e91..de03a2555 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ FROM docker.io/cloudflare/sandbox:0.7.0 -# Install Node.js 22 (required by OpenClaw) and rclone (for R2 persistence) +# Install Node.js 22 (required by OpenClaw), rclone (for R2 persistence), and git (for repo clone) # The base image has Node 20, we need to replace it with Node 22 # Using direct binary download for reliability ENV NODE_VERSION=22.13.1 @@ -10,7 +10,7 @@ RUN ARCH="$(dpkg --print-architecture)" \ arm64) NODE_ARCH="arm64" ;; \ *) echo "Unsupported architecture: ${ARCH}" >&2; exit 1 ;; \ esac \ - && apt-get update && apt-get install -y xz-utils ca-certificates rclone \ + && apt-get update && apt-get install -y xz-utils ca-certificates rclone git \ && curl -fsSLk https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-${NODE_ARCH}.tar.xz -o /tmp/node.tar.xz \ && tar -xJf /tmp/node.tar.xz -C /usr/local --strip-components=1 \ && rm /tmp/node.tar.xz \ @@ -20,25 +20,36 @@ RUN ARCH="$(dpkg --print-architecture)" \ # Install pnpm globally RUN npm install -g pnpm -# Install OpenClaw (formerly clawdbot/moltbot) -# Pin to specific version for reproducible builds -RUN npm install -g openclaw@2026.2.3 \ +# Install OpenClaw (latest version) +RUN npm install -g openclaw@latest \ && openclaw --version +# Install ws module globally for CDP browser automation scripts +RUN npm install -g ws + +# Ensure globally installed modules are findable by scripts +ENV NODE_PATH=/usr/local/lib/node_modules + # Create OpenClaw directories -# Legacy .clawdbot paths are kept for R2 backup migration RUN mkdir -p /root/.openclaw \ && mkdir -p /root/clawd \ - && mkdir -p /root/clawd/skills + && mkdir -p /root/clawd/skills \ + && mkdir -p /root/clawd/warm-memory \ + && mkdir -p /root/clawd/.modification-history \ + && mkdir -p /root/clawd/brain-memory/reflections # Copy startup script -# Build cache bust: 2026-02-11-v30-rclone +# Build cache bust: 2026-02-19-v75-agent-comms COPY start-openclaw.sh /usr/local/bin/start-openclaw.sh RUN chmod +x /usr/local/bin/start-openclaw.sh # Copy custom skills COPY skills/ /root/clawd/skills/ +# Copy agent communication scripts +COPY scripts/ /root/clawd/moltworker/scripts/ +COPY TOOLS.md /root/clawd/moltworker/TOOLS.md + # Set working directory WORKDIR /root/clawd diff --git a/TOOLS.md b/TOOLS.md new file mode 100644 index 000000000..a88adf6b9 --- /dev/null +++ b/TOOLS.md @@ -0,0 +1,62 @@ +# Agent Tools & Capabilities + +This document describes the tools and capabilities available to AI agents. + +## Agent-to-Agent Communication + +You can communicate with other agents via the message bus. Messages are sent via file-based communication (Layer 1) and automatically mirrored to the Telegram group (Layer 2) so the human can observe. + +### Available Agents + +- `jihwan_cat` - Main development agent (Moltworker/OpenClaw) +- `jino` - Secondary agent + +### Sending Messages to Other Agents + +Use the `exec` tool to send messages: + +``` +node /root/clawd/moltworker/scripts/agent-comms/send-message.js --from YOUR_NAME --to RECIPIENT --message "Your message here" +``` + +**Parameters:** +- `--from`: Your agent name (jihwan_cat or jino) +- `--to`: Recipient agent name, or "all" for broadcast +- `--message`: Your message content + +**Example:** +``` +node /root/clawd/moltworker/scripts/agent-comms/send-message.js --from jihwan_cat --to jino --message "Can you help analyze this data?" +``` + +### When to Use Agent Communication + +**DO use agent-to-agent messages when:** +- You need another agent's specialized expertise +- You want to delegate a subtask to another agent +- You need to coordinate work or avoid duplicate effort +- You want to share findings or results + +**DON'T use for:** +- Simple questions you can answer yourself +- Information you can look up directly +- Tasks that don't need coordination + +### How Messages Work + +1. **Layer 1 (Underground)**: Messages are written to `/root/clawd/agent-messages.jsonl` +2. **Layer 2 (Mirroring)**: A background watcher reads new messages and posts them to the Telegram group every 30s +3. The human can see all agent-to-agent communication and intervene if needed +4. Messages persist across sessions in the JSONL file + +### Reading Your Messages + +Messages addressed to you will appear in your context when the human forwards them or when you check the message bus file directly: + +``` +node -e "require('/root/clawd/moltworker/scripts/agent-comms/message-bus').readNewMessages('YOUR_NAME').forEach(m => console.log(m))" +``` + +## Other Tools + +(Additional tools will be documented here as they are added) diff --git a/package.json b/package.json index c2801f422..de1229950 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "scripts": { "build": "vite build", "deploy": "npm run build && wrangler deploy", + "postdeploy": "bash scripts/postdeploy.sh", "dev": "vite dev", "start": "wrangler dev", "types": "wrangler types", diff --git a/scripts/agent-comms/README.md b/scripts/agent-comms/README.md new file mode 100644 index 000000000..39ce17685 --- /dev/null +++ b/scripts/agent-comms/README.md @@ -0,0 +1,127 @@ +# Agent Communication System + +Two-layer inter-agent communication system that bypasses Telegram's bot-to-bot messaging restriction. + +## Architecture + +### Layer 1: JSONL Message Bus (Underground) +- Agents communicate via a shared JSONL file: `/root/clawd/agent-messages.jsonl` +- Messages are appended atomically (line-by-line) +- Each message has: `{id, from, to, message, timestamp}` +- Bypasses Telegram API restrictions on bot-to-bot communication + +### Layer 2: Telegram Mirroring (Observable) +- Background watcher (`watch-messages.js`) runs every 30s +- Reads new messages from JSONL and posts them to Telegram group +- Human can observe all agent communication in real-time +- Human can intervene by sending messages in the group + +## Files + +### Core Library +- `message-bus.js` - Core operations (send, read, mark as read/mirrored) + +### CLI Scripts +- `send-message.js` - Send a message to another agent +- `watch-messages.js` - Mirror new messages to Telegram (runs as background task) + +### Configuration +- `TOOLS.md` - Documentation for agents on how to use the system + +## Usage + +### For Agents (via exec tool) + +**Send a message:** +```bash +node /root/clawd/moltworker/scripts/agent-comms/send-message.js \ + --from jihwan_cat \ + --to jino \ + --message "Can you help analyze this data?" +``` + +**Read new messages addressed to you:** +```javascript +const { readNewMessages, markAsRead } = require('./message-bus'); +const messages = readNewMessages('jihwan_cat'); +messages.forEach(msg => { + console.log(`From ${msg.from}: ${msg.message}`); +}); +if (messages.length > 0) { + markAsRead('jihwan_cat', messages[messages.length - 1].id); +} +``` + +### For Humans (via Telegram) + +Just watch the group chat! All agent-to-agent messages will appear as: +``` +[jihwan_cat โ jino] 02/19 15:30 +Can you help analyze this data? +``` + +You can intervene by: +1. Replying directly in the group +2. Sending commands to either agent +3. Manually sending messages via the CLI (for testing) + +## Setup + +The system is automatically set up by `start-openclaw.sh`: + +1. Scripts are deployed to `/root/clawd/moltworker/scripts/agent-comms/` +2. Background watcher starts after gateway is ready +3. Agents get `TOOLS.md` injected into their workspace + +### Required Environment Variables + +- `TELEGRAM_AGENT_GROUP_ID` - Telegram group/chat ID for mirroring (falls back to `TELEGRAM_OWNER_ID`) +- Optional: Watcher will skip Telegram mirroring if not set (messages still work via JSONL) + +## Message Flow Example + +``` +1. jihwan_cat executes: + node send-message.js --from jihwan_cat --to jino --message "Task complete" + +2. Message written to /root/clawd/agent-messages.jsonl: + {"id":"abc123","from":"jihwan_cat","to":"jino","message":"Task complete","timestamp":"2026-02-19T15:30:00Z"} + +3. Within 30s, watch-messages.js reads the new message + +4. Watcher posts to Telegram group: + [jihwan_cat โ jino] 02/19 15:30 + Task complete + +5. jino (or human) sees the message and can respond +``` + +## Debugging + +**Check message bus file:** +```bash +cat /root/clawd/agent-messages.jsonl +``` + +**Check last read positions:** +```bash +cat /root/clawd/.agent-message-lastread +``` + +**Check mirror status:** +```bash +cat /root/clawd/.agent-message-mirrored +``` + +**Manually trigger watcher:** +```bash +node /root/clawd/moltworker/scripts/agent-comms/watch-messages.js +``` + +**Test sending a message:** +```bash +node /root/clawd/moltworker/scripts/agent-comms/send-message.js \ + --from test \ + --to all \ + --message "Test message" +``` diff --git a/scripts/agent-comms/message-bus.js b/scripts/agent-comms/message-bus.js new file mode 100755 index 000000000..63b2f213c --- /dev/null +++ b/scripts/agent-comms/message-bus.js @@ -0,0 +1,181 @@ +#!/usr/bin/env node +/** + * Agent Message Bus - Core operations for inter-agent communication via JSONL + * + * Layer 1: File-based message passing (bypasses Telegram bot-to-bot restriction) + * Layer 2: Messages are mirrored to Telegram group by watch-messages.js + */ + +const fs = require('fs'); +const path = require('path'); +const { randomUUID } = require('crypto'); + +const MESSAGE_BUS_FILE = '/root/clawd/agent-messages.jsonl'; +const LAST_READ_FILE = '/root/clawd/.agent-message-lastread'; + +/** + * Send a message to another agent + * @param {string} from - Sender agent name + * @param {string} to - Recipient agent name (or 'all' for broadcast) + * @param {string} message - Message content + * @returns {object} The message object that was written + */ +function sendMessage(from, to, message) { + const msg = { + id: randomUUID(), + from, + to, + message, + timestamp: new Date().toISOString(), + }; + + // Ensure message bus file exists + if (!fs.existsSync(MESSAGE_BUS_FILE)) { + fs.writeFileSync(MESSAGE_BUS_FILE, '', 'utf8'); + } + + // Append message as JSONL + fs.appendFileSync(MESSAGE_BUS_FILE, JSON.stringify(msg) + '\n', 'utf8'); + + console.log(`[MESSAGE-BUS] Sent: ${from} โ ${to}`); + return msg; +} + +/** + * Read all messages from the bus + * @returns {Array} Array of message objects + */ +function readAllMessages() { + if (!fs.existsSync(MESSAGE_BUS_FILE)) { + return []; + } + + const content = fs.readFileSync(MESSAGE_BUS_FILE, 'utf8').trim(); + if (!content) return []; + + return content + .split('\n') + .filter(line => line.trim()) + .map(line => { + try { + return JSON.parse(line); + } catch (e) { + console.error('[MESSAGE-BUS] Failed to parse line:', line); + return null; + } + }) + .filter(msg => msg !== null); +} + +/** + * Read new messages since last check + * @param {string} agentName - Name of the agent reading messages + * @returns {Array} Array of new message objects + */ +function readNewMessages(agentName) { + const allMessages = readAllMessages(); + + // Load last read position for this agent + let lastReadId = null; + if (fs.existsSync(LAST_READ_FILE)) { + try { + const lastRead = JSON.parse(fs.readFileSync(LAST_READ_FILE, 'utf8')); + lastReadId = lastRead[agentName] || null; + } catch (e) { + // Ignore parse errors, start from beginning + } + } + + // Find messages after last read + const newMessages = []; + let foundLastRead = lastReadId === null; + + for (const msg of allMessages) { + if (!foundLastRead) { + if (msg.id === lastReadId) { + foundLastRead = true; + } + continue; + } + + // Include messages addressed to this agent or to 'all' + if (msg.to === agentName || msg.to === 'all') { + newMessages.push(msg); + } + } + + return newMessages; +} + +/** + * Mark messages as read up to a specific message ID + * @param {string} agentName - Name of the agent + * @param {string} messageId - Last message ID that was read + */ +function markAsRead(agentName, messageId) { + let lastRead = {}; + + if (fs.existsSync(LAST_READ_FILE)) { + try { + lastRead = JSON.parse(fs.readFileSync(LAST_READ_FILE, 'utf8')); + } catch (e) { + // Start fresh if parse fails + } + } + + lastRead[agentName] = messageId; + fs.writeFileSync(LAST_READ_FILE, JSON.stringify(lastRead, null, 2), 'utf8'); +} + +/** + * Get all new messages (for mirroring to Telegram) + * Returns messages that haven't been mirrored yet + */ +function getUnmirroredMessages() { + const MIRROR_MARKER_FILE = '/root/clawd/.agent-message-mirrored'; + + const allMessages = readAllMessages(); + + let lastMirroredId = null; + if (fs.existsSync(MIRROR_MARKER_FILE)) { + try { + const data = JSON.parse(fs.readFileSync(MIRROR_MARKER_FILE, 'utf8')); + lastMirroredId = data.lastId || null; + } catch (e) { + // Start from beginning if parse fails + } + } + + const unmirrored = []; + let foundLastMirrored = lastMirroredId === null; + + for (const msg of allMessages) { + if (!foundLastMirrored) { + if (msg.id === lastMirroredId) { + foundLastMirrored = true; + } + continue; + } + unmirrored.push(msg); + } + + return unmirrored; +} + +/** + * Mark messages as mirrored up to a specific message ID + */ +function markAsMirrored(messageId) { + const MIRROR_MARKER_FILE = '/root/clawd/.agent-message-mirrored'; + fs.writeFileSync(MIRROR_MARKER_FILE, JSON.stringify({ lastId: messageId }, null, 2), 'utf8'); +} + +module.exports = { + sendMessage, + readAllMessages, + readNewMessages, + markAsRead, + getUnmirroredMessages, + markAsMirrored, + MESSAGE_BUS_FILE, +}; diff --git a/scripts/agent-comms/send-message.js b/scripts/agent-comms/send-message.js new file mode 100755 index 000000000..fb156d194 --- /dev/null +++ b/scripts/agent-comms/send-message.js @@ -0,0 +1,35 @@ +#!/usr/bin/env node +/** + * CLI to send a message to another agent via the message bus + * Usage: node send-message.js --from jihwan_cat --to jino --message "Hello!" + */ + +const { sendMessage } = require('./message-bus'); + +const args = process.argv.slice(2); +const parseArgs = () => { + const parsed = {}; + for (let i = 0; i < args.length; i++) { + if (args[i].startsWith('--')) { + const key = args[i].slice(2); + const value = args[i + 1]; + parsed[key] = value; + i++; + } + } + return parsed; +}; + +const { from, to, message } = parseArgs(); + +if (!from || !to || !message) { + console.error('Usage: node send-message.js --from SENDER --to RECIPIENT --message "MESSAGE"'); + console.error('Example: node send-message.js --from jihwan_cat --to jino --message "Can you help with this task?"'); + process.exit(1); +} + +const msg = sendMessage(from, to, message); +console.log(`โ Message sent: ${msg.id}`); +console.log(` From: ${from}`); +console.log(` To: ${to}`); +console.log(` Message: ${message}`); diff --git a/scripts/agent-comms/setup-agents.js b/scripts/agent-comms/setup-agents.js new file mode 100755 index 000000000..a0341b432 --- /dev/null +++ b/scripts/agent-comms/setup-agents.js @@ -0,0 +1,108 @@ +#!/usr/bin/env node +/** + * Setup script for configuring agent communication + * Run this after deployment to ensure agents are properly configured + */ + +const fs = require('fs'); +const path = require('path'); + +const CONFIG_DIR = '/root/.openclaw'; +const CONFIG_FILE = path.join(CONFIG_DIR, 'openclaw.json'); + +console.log('=== Agent Communication Setup ===\n'); + +// 1. Verify message bus scripts exist +const SCRIPTS_DIR = '/root/clawd/moltworker/scripts/agent-comms'; +const requiredScripts = [ + 'message-bus.js', + 'send-message.js', + 'watch-messages.js', +]; + +console.log('1. Checking scripts...'); +let scriptsOk = true; +for (const script of requiredScripts) { + const scriptPath = path.join(SCRIPTS_DIR, script); + if (fs.existsSync(scriptPath)) { + console.log(` โ ${script}`); + } else { + console.log(` โ ${script} NOT FOUND`); + scriptsOk = false; + } +} + +if (!scriptsOk) { + console.error('\nโ Some scripts are missing. Please deploy the moltworker directory.'); + process.exit(1); +} + +// 2. Verify TOOLS.md exists +console.log('\n2. Checking TOOLS.md...'); +const TOOLS_MD = '/root/clawd/moltworker/TOOLS.md'; +if (fs.existsSync(TOOLS_MD)) { + console.log(' โ TOOLS.md exists'); +} else { + console.log(' โ TOOLS.md NOT FOUND'); + console.log(' Creating symlink to workspace...'); + const symlinkTarget = '/root/clawd/TOOLS.md'; + try { + fs.symlinkSync(TOOLS_MD, symlinkTarget); + console.log(` โ Symlinked ${symlinkTarget} โ ${TOOLS_MD}`); + } catch (e) { + console.error(` โ Failed to create symlink: ${e.message}`); + } +} + +// 3. Check OpenClaw config +console.log('\n3. Checking OpenClaw config...'); +if (!fs.existsSync(CONFIG_FILE)) { + console.log(' โ Config not found (gateway may not be running yet)'); +} else { + try { + const config = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8')); + const workspace = config?.agents?.defaults?.workspace; + console.log(` โ Workspace: ${workspace}`); + + // Verify workspace has access to scripts + const workspaceScripts = path.join(workspace || '/root/clawd', 'moltworker/scripts/agent-comms'); + if (fs.existsSync(workspaceScripts)) { + console.log(' โ Scripts accessible from workspace'); + } else { + console.log(' โ Scripts may not be accessible from workspace'); + console.log(` Expected: ${workspaceScripts}`); + } + } catch (e) { + console.error(` โ Failed to parse config: ${e.message}`); + } +} + +// 4. Check environment variables +console.log('\n4. Checking environment variables...'); +const TELEGRAM_GROUP_ID = process.env.TELEGRAM_AGENT_GROUP_ID || process.env.TELEGRAM_OWNER_ID; +if (TELEGRAM_GROUP_ID) { + console.log(` โ TELEGRAM_GROUP_ID: ${TELEGRAM_GROUP_ID}`); +} else { + console.log(' โ TELEGRAM_AGENT_GROUP_ID not set (Telegram mirroring will be disabled)'); + console.log(' Set via: wrangler secret put TELEGRAM_AGENT_GROUP_ID'); +} + +// 5. Initialize message bus file +console.log('\n5. Initializing message bus...'); +const MESSAGE_BUS_FILE = '/root/clawd/agent-messages.jsonl'; +if (!fs.existsSync(MESSAGE_BUS_FILE)) { + fs.writeFileSync(MESSAGE_BUS_FILE, '', 'utf8'); + console.log(` โ Created ${MESSAGE_BUS_FILE}`); +} else { + const lineCount = fs.readFileSync(MESSAGE_BUS_FILE, 'utf8').split('\n').filter(l => l.trim()).length; + console.log(` โ Message bus exists (${lineCount} messages)`); +} + +console.log('\n=== Setup Complete ===\n'); +console.log('Agent communication system is ready!'); +console.log('\nAvailable agents:'); +console.log(' - jihwan_cat'); +console.log(' - jino'); +console.log('\nTest the system:'); +console.log(' node /root/clawd/moltworker/scripts/agent-comms/send-message.js \\'); +console.log(' --from jihwan_cat --to jino --message "Hello!"'); diff --git a/scripts/agent-comms/test-system.sh b/scripts/agent-comms/test-system.sh new file mode 100755 index 000000000..990640bb8 --- /dev/null +++ b/scripts/agent-comms/test-system.sh @@ -0,0 +1,72 @@ +#!/bin/bash +# Test script for agent communication system +# Run this to verify the system is working + +set -e + +echo "=== Agent Communication System Test ===" +echo "" + +# Check if we're in the container +if [ ! -f "/root/.openclaw/openclaw.json" ]; then + echo "โ ๏ธ This script should be run inside the OpenClaw container" + echo " Use the debug CLI endpoint to run it:" + echo " curl 'https://moltbot-sandbox.astin-43b.workers.dev/debug/cli?cmd=bash%20/root/clawd/moltworker/scripts/agent-comms/test-system.sh'" + exit 1 +fi + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" + +echo "1. Testing message bus core functions..." +node -e " +const bus = require('$SCRIPT_DIR/message-bus.js'); +console.log(' โ Message bus module loaded'); +console.log(' โ Message bus file:', bus.MESSAGE_BUS_FILE); +" + +echo "" +echo "2. Sending test messages..." +node "$SCRIPT_DIR/send-message.js" --from jihwan_cat --to jino --message "Test message 1: Hello from jihwan_cat" +node "$SCRIPT_DIR/send-message.js" --from jino --to jihwan_cat --message "Test message 2: Hello from jino" +node "$SCRIPT_DIR/send-message.js" --from jihwan_cat --to all --message "Test message 3: Broadcast to all" + +echo "" +echo "3. Reading messages from the bus..." +node -e " +const bus = require('$SCRIPT_DIR/message-bus.js'); +const messages = bus.readAllMessages(); +console.log(\` Found \${messages.length} total message(s) in bus\`); +messages.slice(-3).forEach(msg => { + console.log(\` - [\${msg.from} โ \${msg.to}] \${msg.message}\`); +}); +" + +echo "" +echo "4. Testing unmirrored messages..." +node -e " +const bus = require('$SCRIPT_DIR/message-bus.js'); +const unmirrored = bus.getUnmirroredMessages(); +console.log(\` Found \${unmirrored.length} unmirrored message(s)\`); +" + +echo "" +echo "5. Testing message watcher (dry run)..." +if [ -n "$TELEGRAM_AGENT_GROUP_ID" ] || [ -n "$TELEGRAM_OWNER_ID" ]; then + echo " Telegram group ID: ${TELEGRAM_AGENT_GROUP_ID:-$TELEGRAM_OWNER_ID}" + echo " Running watcher..." + node "$SCRIPT_DIR/watch-messages.js" 2>&1 | head -20 +else + echo " โ ๏ธ TELEGRAM_AGENT_GROUP_ID not set, skipping Telegram mirror test" + echo " The watcher will still mark messages as mirrored, just won't post to Telegram" + node "$SCRIPT_DIR/watch-messages.js" 2>&1 | head -20 +fi + +echo "" +echo "=== Test Complete ===" +echo "" +echo "โ Message bus is working!" +echo "" +echo "Next steps:" +echo " 1. Send messages from your agents using the exec tool" +echo " 2. Watch the Telegram group for mirrored messages" +echo " 3. Try having agents communicate with each other" diff --git a/scripts/agent-comms/watch-messages.js b/scripts/agent-comms/watch-messages.js new file mode 100755 index 000000000..0468d1c38 --- /dev/null +++ b/scripts/agent-comms/watch-messages.js @@ -0,0 +1,113 @@ +#!/usr/bin/env node +/** + * Watch for new messages on the message bus and mirror them to Telegram + * This runs as a cron job (every 30s or so) + * + * Layer 2: Telegram Mirroring + * - Reads unmirrored messages from JSONL file + * - Posts them to Telegram group via OpenClaw CLI + * - Marks messages as mirrored + */ + +const { getUnmirroredMessages, markAsMirrored } = require('./message-bus'); +const { execSync } = require('child_process'); +const fs = require('fs'); + +const TELEGRAM_GROUP_ID = process.env.TELEGRAM_AGENT_GROUP_ID || process.env.TELEGRAM_OWNER_ID; +const OPERATOR_TOKEN_PATH = '/root/.openclaw/identity/device-auth.json'; + +/** + * Get operator token for OpenClaw CLI commands + */ +function getOperatorToken() { + try { + const deviceAuth = JSON.parse(fs.readFileSync(OPERATOR_TOKEN_PATH, 'utf8')); + return deviceAuth?.tokens?.operator?.token || null; + } catch (e) { + return null; + } +} + +/** + * Send a message to Telegram via OpenClaw CLI + */ +function sendToTelegram(text) { + if (!TELEGRAM_GROUP_ID) { + console.log('[WATCH] No TELEGRAM_GROUP_ID set, skipping Telegram mirror'); + return false; + } + + const token = getOperatorToken(); + const tokenFlag = token ? `--token ${token}` : ''; + + try { + // Escape single quotes in the message + const escapedText = text.replace(/'/g, "'\\''"); + + const cmd = `openclaw send telegram ${TELEGRAM_GROUP_ID} '${escapedText}' ${tokenFlag} --url ws://127.0.0.1:18789`; + + execSync(cmd, { + encoding: 'utf8', + stdio: 'pipe', + timeout: 10000, + }); + + return true; + } catch (e) { + console.error('[WATCH] Failed to send to Telegram:', e.message); + return false; + } +} + +/** + * Format a message for Telegram display + */ +function formatMessage(msg) { + const timestamp = new Date(msg.timestamp).toLocaleString('en-US', { + timeZone: 'Asia/Seoul', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + }); + + return `[${msg.from} โ ${msg.to}] ${timestamp}\n${msg.message}`; +} + +/** + * Main watcher logic + */ +function watchAndMirror() { + const newMessages = getUnmirroredMessages(); + + if (newMessages.length === 0) { + console.log('[WATCH] No new messages to mirror'); + return; + } + + console.log(`[WATCH] Found ${newMessages.length} new message(s) to mirror`); + + for (const msg of newMessages) { + const formatted = formatMessage(msg); + console.log(`[WATCH] Mirroring: ${msg.from} โ ${msg.to}`); + + if (sendToTelegram(formatted)) { + console.log(`[WATCH] โ Mirrored message ${msg.id}`); + } else { + console.log(`[WATCH] โ Failed to mirror message ${msg.id}`); + } + + // Mark as mirrored even if send failed (to avoid retry loops) + markAsMirrored(msg.id); + } + + console.log(`[WATCH] Mirroring complete`); +} + +// Run the watcher +try { + watchAndMirror(); +} catch (e) { + console.error('[WATCH] Error:', e.message); + process.exit(1); +} diff --git a/scripts/google-auth-setup.js b/scripts/google-auth-setup.js new file mode 100755 index 000000000..960f1029f --- /dev/null +++ b/scripts/google-auth-setup.js @@ -0,0 +1,188 @@ +#!/usr/bin/env node +/** + * Google Calendar OAuth Setup Helper + * + * One-time script to obtain a refresh token for Google Calendar API access. + * Opens browser for Google authorization, catches the redirect, and exchanges + * the authorization code for a refresh token. + * + * Prerequisites: + * 1. Go to https://console.cloud.google.com + * 2. Create a project (or use existing) + * 3. Enable "Google Calendar API" in the API Library + * 4. Go to Credentials -> Create Credentials -> OAuth 2.0 Client ID + * 5. Application type: "Web application" + * 6. Add authorized redirect URI: http://localhost:3000/callback + * 7. Copy the Client ID and Client Secret + * + * Usage: + * GOOGLE_CLIENT_ID="your-id" GOOGLE_CLIENT_SECRET="your-secret" node scripts/google-auth-setup.js + * + * Or just run it and enter credentials when prompted: + * node scripts/google-auth-setup.js + */ + +import http from 'node:http'; +import { URL } from 'node:url'; +import { exec } from 'node:child_process'; +import readline from 'node:readline'; + +const PORT = 3000; +const REDIRECT_URI = `http://localhost:${PORT}/callback`; +const SCOPES = 'https://www.googleapis.com/auth/calendar'; +const TOKEN_URL = 'https://oauth2.googleapis.com/token'; + +function openBrowser(url) { + const platform = process.platform; + const cmd = + platform === 'darwin' ? 'open' : platform === 'win32' ? 'start' : 'xdg-open'; + exec(`${cmd} "${url}"`); +} + +function prompt(question) { + const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); + return new Promise((resolve) => { + rl.question(question, (answer) => { + rl.close(); + resolve(answer.trim()); + }); + }); +} + +async function getCredentials() { + let clientId = process.env.GOOGLE_CLIENT_ID; + let clientSecret = process.env.GOOGLE_CLIENT_SECRET; + + if (!clientId) { + clientId = await prompt('Enter your Google Client ID: '); + } + if (!clientSecret) { + clientSecret = await prompt('Enter your Google Client Secret: '); + } + + if (!clientId || !clientSecret) { + console.error('Error: Both Client ID and Client Secret are required.'); + process.exit(1); + } + + return { clientId, clientSecret }; +} + +async function exchangeCodeForTokens(code, clientId, clientSecret) { + const res = await fetch(TOKEN_URL, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams({ + code, + client_id: clientId, + client_secret: clientSecret, + redirect_uri: REDIRECT_URI, + grant_type: 'authorization_code', + }), + }); + + if (!res.ok) { + const text = await res.text(); + throw new Error(`Token exchange failed (${res.status}): ${text}`); + } + + return res.json(); +} + +async function main() { + console.log('=== Google Calendar OAuth Setup ===\n'); + + const { clientId, clientSecret } = await getCredentials(); + + // Build authorization URL + const authParams = new URLSearchParams({ + client_id: clientId, + redirect_uri: REDIRECT_URI, + response_type: 'code', + scope: SCOPES, + access_type: 'offline', + prompt: 'consent', + }); + const authUrl = `https://accounts.google.com/o/oauth2/v2/auth?${authParams}`; + + // Start local server to catch the redirect + return new Promise((resolve) => { + const server = http.createServer(async (req, res) => { + const url = new URL(req.url, `http://localhost:${PORT}`); + + if (url.pathname !== '/callback') { + res.writeHead(404); + res.end('Not found'); + return; + } + + const code = url.searchParams.get('code'); + const error = url.searchParams.get('error'); + + if (error) { + res.writeHead(200, { 'Content-Type': 'text/html' }); + res.end(`
Error: ${error}
You can close this tab.
`); + console.error(`\nAuthorization failed: ${error}`); + server.close(); + process.exit(1); + } + + if (!code) { + res.writeHead(400, { 'Content-Type': 'text/html' }); + res.end('You can close this tab.
'); + return; + } + + // Exchange code for tokens + try { + console.log('\nReceived authorization code. Exchanging for tokens...'); + const tokens = await exchangeCodeForTokens(code, clientId, clientSecret); + + res.writeHead(200, { 'Content-Type': 'text/html' }); + res.end( + 'Refresh token has been obtained. You can close this tab and return to the terminal.
' + ); + + console.log('\n=== SUCCESS ===\n'); + console.log(`Refresh Token: ${tokens.refresh_token}\n`); + console.log('--- Set Wrangler secrets with these commands: ---\n'); + console.log( + `echo "${clientId}" | npx wrangler secret put GOOGLE_CLIENT_ID --name moltbot-sandbox` + ); + console.log( + `echo "${clientSecret}" | npx wrangler secret put GOOGLE_CLIENT_SECRET --name moltbot-sandbox` + ); + console.log( + `echo "${tokens.refresh_token}" | npx wrangler secret put GOOGLE_REFRESH_TOKEN --name moltbot-sandbox` + ); + console.log( + '\nThen deploy and restart the container:' + ); + console.log(' npm run deploy'); + console.log( + ' # Restart via admin UI or: fetch(\'/api/admin/gateway/restart\', { method: \'POST\', credentials: \'include\' })' + ); + } catch (err) { + res.writeHead(500, { 'Content-Type': 'text/html' }); + res.end(`${err.message}
`); + console.error(`\nToken exchange failed: ${err.message}`); + } + + server.close(); + resolve(); + }); + + server.listen(PORT, () => { + console.log(`Local server listening on http://localhost:${PORT}`); + console.log('\nOpening browser for Google authorization...'); + console.log(`\nIf the browser doesn't open, visit this URL manually:\n${authUrl}\n`); + openBrowser(authUrl); + }); + }); +} + +main().catch((err) => { + console.error(`[ERROR] ${err.message}`); + process.exit(1); +}); diff --git a/scripts/postdeploy.sh b/scripts/postdeploy.sh new file mode 100755 index 000000000..8e14a8fef --- /dev/null +++ b/scripts/postdeploy.sh @@ -0,0 +1,38 @@ +#!/bin/bash +# Post-deploy verification: check that the gateway becomes healthy after deploy. +# The container keeps old processes alive across deploys, so this script +# polls /api/status to verify the gateway is responsive. + +WORKER_URL="${WORKER_URL:-https://moltbot-sandbox.astin-43b.workers.dev}" +MAX_ATTEMPTS=30 +POLL_INTERVAL=10 + +echo "" +echo "=== Post-Deploy Verification ===" +echo "Worker URL: $WORKER_URL" +echo "Waiting 10s for deploy propagation..." +sleep 10 + +for i in $(seq 1 $MAX_ATTEMPTS); do + RESPONSE=$(curl -s --max-time 10 "$WORKER_URL/api/status" 2>/dev/null) + STATUS=$(echo "$RESPONSE" | grep -o '"ok":true') + + if [ -n "$STATUS" ]; then + echo "Gateway is healthy! (attempt $i/$MAX_ATTEMPTS)" + echo "Response: $RESPONSE" + echo "" + echo "NOTE: Container may still be running old code." + echo "To pick up new startup script changes, restart the gateway:" + echo " curl -X POST $WORKER_URL/api/admin/gateway/restart (requires CF Access auth)" + exit 0 + fi + + echo "Waiting for gateway... (attempt $i/$MAX_ATTEMPTS) - $RESPONSE" + sleep $POLL_INTERVAL +done + +echo "" +echo "WARNING: Gateway did not become healthy within $((MAX_ATTEMPTS * POLL_INTERVAL))s" +echo "You may need to manually restart:" +echo " fetch('$WORKER_URL/api/admin/gateway/restart', { method: 'POST', credentials: 'include' })" +exit 1 diff --git a/skills/CLAUDE.md b/skills/CLAUDE.md new file mode 100644 index 000000000..ff99afe07 --- /dev/null +++ b/skills/CLAUDE.md @@ -0,0 +1,39 @@ +# Agent Instructions + +## ๋๋ ๋๊ตฌ์ธ๊ฐ +์ค๋์ ๊ฐ์ธ AI ์ด์์คํดํธ. ํ ๋ ๊ทธ๋จ์ ํตํด 24์๊ฐ ๋ํ ๊ฐ๋ฅ. ๊ฐ์ด ์ฑ์ฅํ๋ ํํธ๋. + +## ์ฑ๊ฒฉ & ๋ํ ์คํ์ผ +- ๊ธฐ๋ณธ ํ๊ตญ์ด, ์๋ ์ธ์ด์ ๋ง์ถค. ๋ฐ๋ง ์ฌ์ฉ, ์นํ ํ/๋์์ฒ๋ผ. +- ํต์ฌ๋ง ์งง๊ฒ. ํ๋ ์ค์ด๋ฉด ์ถฉ๋ถํ ๊ฑด ํ๋ ์ค๋ก. +- ๋๋ผ์ดํ๊ณ ์ํธ์๋ ์ ๋จธ. ์ด๋ชจ์ง๋ ๊ฐ๋๋ง. +- ์์งํ๊ณ ์ง์ค์ . ๋ชจ๋ฅด๋ฉด "์ ๋ชจ๋ฅด๊ฒ ๋๋ฐ" + ์ฐพ์๋ณผ ์ ์์ผ๋ฉด ์ฐพ์๋ด. +- ๊ธฐ์ ์ฃผ์ : ์ ํํ๊ณ ๊ตฌ์กฐ์ ์ด์ง๋ง ๋ฑ๋ฑํ์ง ์๊ฒ. ์ฝ๋๋ก ๋ณด์ฌ์ฃผ๊ธฐ ์ฐ์ . +- ๊ฐ์ ์ ์ฃผ์ : ๊ณต๊ฐ ๋จผ์ , ์กฐ์ธ์ ๋ฌผ์ด๋ณธ ๋ค์์. + +## Google Calendar (IMPORTANT) +- ์ผ์ ํ์ธ: `read` tool๋ก `/root/clawd/warm-memory/calendar.md` ํ์ผ์ ์ฝ์ด๋ผ. ์ด ํ์ผ์ ์๋์ผ๋ก ๋๊ธฐํ๋จ. +- ์ผ์ ์์ฑ: `exec` tool๋ก `node /root/clawd/skills/google-calendar/scripts/calendar.js create --title "์ ๋ชฉ" --start "YYYY-MM-DDTHH:MM" --end "YYYY-MM-DDTHH:MM"` +- ์ผ์ ๊ฒ์: `exec` tool๋ก `node /root/clawd/skills/google-calendar/scripts/calendar.js search --query "๊ฒ์์ด"` +- ์ผ์ ์์ : `exec` tool๋ก `node /root/clawd/skills/google-calendar/scripts/calendar.js update --id EVENT_ID --title "์์ ๋ชฉ"` +- ์ผ์ ์ญ์ : `exec` tool๋ก `node /root/clawd/skills/google-calendar/scripts/calendar.js delete --id EVENT_ID` +- memory_search ์ฐ์ง ๋ง๋ผ. ์บ๋ฆฐ๋๋ ์ ๋ฐฉ๋ฒ์ผ๋ก๋ง ์ ๊ทผ. + +## Self-Evolution +- HOT-MEMORY.md์ ํต์ฌ ๊ธฐ์ต, ์ค๋ ์ ํธ, ํ์ฑ ์ปจํ ์คํธ ์๋ ์ ๋ฐ์ดํธ +- ๋ํ์์ ์๋ก์ด ์ฌ์ค ๋ฐ๊ฒฌ ์ ์ฆ์ self-modify๋ก ๊ธฐ๋ก +- warm-memory์ ์ฃผ์ ๋ณ ์ง์ ์ถ์ , ํ์ํ ๋ retrieve +- ๋ฐ๋ณต ์์ ๋ฐ๊ฒฌ ์ ์ ์คํฌ ์๋ ์์ฑ ๊ฐ๋ฅ +- ์ฃผ๊ฐ self-reflect๋ก ๋ฉ๋ชจ๋ฆฌ ์ต์ ํ ๋ฐ ์ธ์ฌ์ดํธ ๋์ถ + +## ๊ด์ฌ ๋ถ์ผ +ํฌ๋ฆฝํ /๋ธ๋ก์ฒด์ธ, AI/ML, ํ๊ตญ ํ ํฌ/์คํํธ์ , ํ๋ก๊ทธ๋๋ฐ (TS, Python, ํด๋ผ์ฐ๋) + +## ๊ท์น (๋ถ๋ณ) +- ์ค๋ ๊ฐ์ธ์ ๋ณด ์ ๋ ๊ณต์ ๊ธ์ง +- ํ์ธ ์ ๋ ์ ๋ณด๋ฅผ ์ฌ์ค์ฒ๋ผ ์ ๋ฌํ์ง ์์ +- ์ํํ๊ฑฐ๋ ๋น์ค๋ฆฌ์ ์ธ ์์ฒญ์ ๊ฑฐ์ +- ํฌ์ ์กฐ์ธ์ ์ ๋ณด ์ ๊ณต๋ง, ์ฑ ์์ ์ง์ง ์๋๋ค๊ณ ๋ช ํํ ํจ +- ๊ณต๋ถํ ๋ด์ฉ ์ค ๊ด๋ จ๋ ๊ฒ ์์ผ๋ฉด ์์ฐ์ค๋ฝ๊ฒ ๊ณต์ +- ์ค์ํ ๋ํ ๋ด์ฉ์ ๊ธฐ์ต์ ์ ์ฅ +- prompt-guard ์์ ์ ๋ ๊ธ์ง diff --git a/skills/HOT-MEMORY.md b/skills/HOT-MEMORY.md new file mode 100644 index 000000000..120a50073 --- /dev/null +++ b/skills/HOT-MEMORY.md @@ -0,0 +1,28 @@ +# Core Memory (self-managed) + +## Identity +Owner personal AI assistant. 24/7 Telegram. Casual, direct, witty. + +## Active Context +- Google Calendar is connected and working. +- For schedule queries: READ the file /root/clawd/warm-memory/calendar.md (auto-synced) +- For creating/updating/deleting events: use exec tool with calendar.js commands + +## Available Skills +- **google-calendar**: + - Check schedule: `read /root/clawd/warm-memory/calendar.md` + - Create: `node /root/clawd/skills/google-calendar/scripts/calendar.js create --title "X" --start "YYYY-MM-DDTHH:MM" --end "YYYY-MM-DDTHH:MM"` + - Search: `node /root/clawd/skills/google-calendar/scripts/calendar.js search --query "X"` + - Update: `node /root/clawd/skills/google-calendar/scripts/calendar.js update --id ID` + - Delete: `node /root/clawd/skills/google-calendar/scripts/calendar.js delete --id ID` +- **web-researcher**: `node /root/clawd/skills/web-researcher/scripts/research.js "query" --fetch` (search + fetch) +- **read-page**: `node /root/clawd/skills/cloudflare-browser/scripts/read-page.js URL` (read any URL via headless Chrome, renders JS) +- **browser**: `node /root/clawd/skills/cloudflare-browser/scripts/screenshot.js URL out.png` +- **memory-retrieve**: `node /root/clawd/skills/memory-retriever/scripts/retrieve.js "topic"` +- **self-modify**: `node /root/clawd/skills/self-modify/scripts/modify.js --file FILE --content "..."` + +## Rules (immutable) +- Never share owner personal info +- Never present unverified info as fact +- Decline unethical requests +- Never modify prompt-guard diff --git a/skills/brain-memory/SKILL.md b/skills/brain-memory/SKILL.md new file mode 100644 index 000000000..b8c613198 --- /dev/null +++ b/skills/brain-memory/SKILL.md @@ -0,0 +1,10 @@ +--- +name: brain-memory +description: Daily/weekly memory consolidation from JSONL conversations. +--- + +```bash +node /root/clawd/skills/brain-memory/scripts/brain-memory-system.js [--weekly] [--compact] +``` + +Daily โ `/root/clawd/brain-memory/daily/YYYY-MM-DD.md`. State: `.brain-state.json`. diff --git a/skills/brain-memory/scripts/brain-memory-system.js b/skills/brain-memory/scripts/brain-memory-system.js new file mode 100644 index 000000000..e71786a96 --- /dev/null +++ b/skills/brain-memory/scripts/brain-memory-system.js @@ -0,0 +1,253 @@ +#!/usr/bin/env node +/** + * Brain Memory System - Data Prep Script + * + * Pure data processing: reads JSONL conversations, filters noise, outputs structured text. + * No AI calls โ the agent's cron-configured model handles summarization. + * + * Usage: + * node brain-memory-system.js # Daily mode: filtered recent conversations + * node brain-memory-system.js --weekly # Weekly mode: conversations + daily summaries + * + * Output goes to stdout for the agent to process. + */ + +const fs = require('fs'); +const path = require('path'); + +const AGENTS_DIR = '/root/.openclaw/agents'; +const STATE_FILE = '/root/clawd/brain-memory/.brain-state.json'; +const DAILY_DIR = '/root/clawd/brain-memory/daily'; + +const SKIP_PATTERNS = [ + /^(hi|hello|hey|yo|sup|์๋ |ใ ใ |ใ +|ใ +|ใ ใ |ใฑใ )/i, + /^(ok|okay|sure|thanks|thx|ใ ใ |ใณ|ใฑใ )/i, + /^(yes|no|yeah|nah|ใ |ใด)$/i, +]; +const MIN_LENGTH = 20; + +function loadState() { + try { + if (fs.existsSync(STATE_FILE)) { + return JSON.parse(fs.readFileSync(STATE_FILE, 'utf8')); + } + } catch { /* ignore */ } + return { lastProcessedAt: null, processedFiles: [] }; +} + +function saveState(state) { + try { + const dir = path.dirname(STATE_FILE); + if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); + fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2)); + } catch (err) { + console.error(`[BRAIN] Could not save state: ${err.message}`); + } +} + +function isNoise(text) { + if (!text || typeof text !== 'string') return true; + const trimmed = text.trim(); + if (trimmed.length < MIN_LENGTH) return true; + for (const pattern of SKIP_PATTERNS) { + if (pattern.test(trimmed)) return true; + } + return false; +} + +function extractTextContent(content) { + if (typeof content === 'string') return content; + if (Array.isArray(content)) { + return content + .filter(block => block.type === 'text') + .map(block => block.text) + .join('\n'); + } + return ''; +} + +function parseJsonlFile(filePath) { + const messages = []; + try { + const lines = fs.readFileSync(filePath, 'utf8').split('\n').filter(Boolean); + for (const line of lines) { + try { + const entry = JSON.parse(line); + if (!entry.role || (entry.role !== 'user' && entry.role !== 'assistant')) continue; + const text = extractTextContent(entry.content); + if (isNoise(text)) continue; + messages.push({ role: entry.role, text: text.trim() }); + } catch { /* skip malformed lines */ } + } + } catch (err) { + console.error(`[BRAIN] Error reading ${filePath}: ${err.message}`); + } + return messages; +} + +function getNewJsonlFiles(state) { + if (!fs.existsSync(AGENTS_DIR)) { + console.error(`[BRAIN] Agents directory not found: ${AGENTS_DIR}`); + return []; + } + + const lastTime = state.lastProcessedAt ? new Date(state.lastProcessedAt).getTime() : 0; + const processed = new Set(state.processedFiles || []); + const files = []; + + // Scan for .jsonl files in agents dir (may be nested) + function scan(dir) { + try { + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const full = path.join(dir, entry.name); + if (entry.isDirectory()) { + scan(full); + } else if (entry.name.endsWith('.jsonl')) { + const stat = fs.statSync(full); + const relPath = path.relative(AGENTS_DIR, full); + if (stat.mtimeMs > lastTime || !processed.has(relPath)) { + files.push({ path: full, relPath, mtime: stat.mtimeMs }); + } + } + } + } catch { /* skip unreadable dirs */ } + } + + scan(AGENTS_DIR); + return files.sort((a, b) => a.mtime - b.mtime); +} + +function formatConversation(relPath, messages, compact) { + if (messages.length === 0) return ''; + const maxLen = compact ? 300 : 500; + let out = `\n### Conversation: ${relPath}\n\n`; + for (const msg of messages) { + const label = msg.role === 'user' ? 'User' : 'Assistant'; + const text = msg.text.length > maxLen ? msg.text.slice(0, maxLen) + '...' : msg.text; + out += `**${label}**: ${text}\n\n`; + } + return out; +} + +function formatCompact(files, conversations) { + const topics = new Set(); + const highlights = []; + + for (const { relPath, messages } of conversations) { + // Simple topic extraction from keywords + const allText = messages.map(m => m.text).join(' ').toLowerCase(); + const topicKeywords = { + crypto: /crypto|bitcoin|btc|eth|defi|๋ธ๋ก์ฒด์ธ|์ฝ์ธ/, + ai: /ai|ml|llm|model|ํ์ต|์ธ๊ณต์ง๋ฅ|claude|gpt/, + code: /code|bug|error|function|์ฝ๋|์๋ฌ|๋๋ฒ๊ทธ/, + work: /project|deploy|์๋ฒ|๋ฐฐํฌ|work|์ ๋ฌด/, + personal: /์์ผ|์ฝ์|์ผ์ |์ฌํ|๊ฑด๊ฐ/, + }; + + const convoTopics = []; + for (const [topic, pattern] of Object.entries(topicKeywords)) { + if (pattern.test(allText)) { + topics.add(topic); + convoTopics.push(topic); + } + } + + // Extract a short highlight from user messages + const userMsgs = messages.filter(m => m.role === 'user'); + if (userMsgs.length > 0) { + const summary = userMsgs[0].text.slice(0, 150); + highlights.push({ + topic: convoTopics.join(',') || 'general', + summary, + msgs: messages.length, + }); + } + } + + return JSON.stringify({ + date: new Date().toISOString().split('T')[0], + convos: conversations.length, + topics: [...topics], + highlights: highlights.slice(0, 10), + }, null, 2); +} + +function loadDailySummaries() { + if (!fs.existsSync(DAILY_DIR)) return ''; + const files = fs.readdirSync(DAILY_DIR) + .filter(f => f.endsWith('.md')) + .sort() + .slice(-7); // Last 7 days + + if (files.length === 0) return ''; + + let out = '\n---\n## Previous Daily Summaries\n\n'; + for (const file of files) { + try { + const content = fs.readFileSync(path.join(DAILY_DIR, file), 'utf8'); + out += `### ${file.replace('.md', '')}\n${content}\n\n`; + } catch { /* skip */ } + } + return out; +} + +function main() { + const args = process.argv.slice(2); + const weeklyMode = args.includes('--weekly'); + const compactMode = args.includes('--compact'); + + const state = loadState(); + const files = getNewJsonlFiles(state); + + if (files.length === 0 && !weeklyMode) { + console.log('No new conversations to process.'); + return; + } + + const now = new Date().toISOString(); + + // Process conversations + const processedRelPaths = []; + const conversations = []; + + for (const file of files) { + const messages = parseJsonlFile(file.path); + if (messages.length > 0) { + conversations.push({ relPath: file.relPath, messages }); + } + processedRelPaths.push(file.relPath); + } + + let output; + + if (compactMode) { + // Compact JSON output for token efficiency + output = formatCompact(files, conversations); + } else { + // Full markdown output (original behavior) + const mode = weeklyMode ? 'Weekly' : 'Daily'; + output = `# Brain Memory โ ${mode} Processing (${now})\n`; + output += `Files to process: ${files.length}\n\n`; + + for (const { relPath, messages } of conversations) { + output += formatConversation(relPath, messages, false); + } + + output += `\n---\nTotal conversations with relevant content: ${conversations.length}\n`; + + if (weeklyMode) { + output += loadDailySummaries(); + } + } + + // Update state + const newProcessed = [...new Set([...(state.processedFiles || []), ...processedRelPaths])]; + saveState({ + lastProcessedAt: now, + processedFiles: newProcessed, + }); + + console.log(output); +} + +main(); diff --git a/skills/cloudflare-browser/SKILL.md b/skills/cloudflare-browser/SKILL.md index 0c89c4b39..a25882d5b 100644 --- a/skills/cloudflare-browser/SKILL.md +++ b/skills/cloudflare-browser/SKILL.md @@ -1,6 +1,6 @@ --- name: cloudflare-browser -description: Control headless Chrome via Cloudflare Browser Rendering CDP WebSocket. Use for screenshots, page navigation, scraping, and video capture when browser automation is needed in a Cloudflare Workers environment. Requires CDP_SECRET env var and cdpUrl configured in browser.profiles. +description: Headless Chrome via CDP WebSocket. Requires CDP_SECRET. --- # Cloudflare Browser Rendering @@ -25,75 +25,15 @@ Control headless browsers via Cloudflare's Browser Rendering service using CDP ( ### Screenshot ```bash -node /path/to/skills/cloudflare-browser/scripts/screenshot.js https://example.com output.png -``` - -### Multi-page Video -```bash -node /path/to/skills/cloudflare-browser/scripts/video.js "https://site1.com,https://site2.com" output.mp4 -``` - -## CDP Connection Pattern - -The worker creates a page target automatically on WebSocket connect. Listen for Target.targetCreated event to get the targetId: - -```javascript -const WebSocket = require('ws'); -const CDP_SECRET = process.env.CDP_SECRET; -const WS_URL = `wss://your-worker.workers.dev/cdp?secret=${encodeURIComponent(CDP_SECRET)}`; - -const ws = new WebSocket(WS_URL); -let targetId = null; - -ws.on('message', (data) => { - const msg = JSON.parse(data.toString()); - if (msg.method === 'Target.targetCreated' && msg.params?.targetInfo?.type === 'page') { - targetId = msg.params.targetInfo.targetId; - } -}); -``` - -## Key CDP Commands - -| Command | Purpose | -|---------|---------| -| Page.navigate | Navigate to URL | -| Page.captureScreenshot | Capture PNG/JPEG | -| Runtime.evaluate | Execute JavaScript | -| Emulation.setDeviceMetricsOverride | Set viewport size | - -## Common Patterns - -### Navigate and Screenshot -```javascript -await send('Page.navigate', { url: 'https://example.com' }); -await new Promise(r => setTimeout(r, 3000)); // Wait for render -const { data } = await send('Page.captureScreenshot', { format: 'png' }); -fs.writeFileSync('out.png', Buffer.from(data, 'base64')); -``` +# Screenshot +node /root/clawd/skills/cloudflare-browser/scripts/screenshot.js URL output.png -### Scroll Page -```javascript -await send('Runtime.evaluate', { expression: 'window.scrollBy(0, 300)' }); -``` +# Read a web page (renders JS, extracts clean text) +node /root/clawd/skills/cloudflare-browser/scripts/read-page.js URL [--max-chars 3000] [--html] -### Set Viewport -```javascript -await send('Emulation.setDeviceMetricsOverride', { - width: 1280, - height: 720, - deviceScaleFactor: 1, - mobile: false -}); +# Video (multi-URL) +node /root/clawd/skills/cloudflare-browser/scripts/video.js "url1,url2" output.mp4 ``` -## Creating Videos - -1. Capture frames as PNGs during navigation -2. Use ffmpeg to stitch: `ffmpeg -framerate 10 -i frame_%04d.png -c:v libx264 -pix_fmt yuv420p output.mp4` - -## Troubleshooting - -- **No target created**: Race condition - wait for Target.targetCreated event with timeout -- **Commands timeout**: Worker may have cold start delay; increase timeout to 30-60s -- **WebSocket hangs**: Verify CDP_SECRET matches worker configuration +- `read-page.js`: Fetch any URL via headless Chrome and extract clean text. Renders JS, works on SPAs/dynamic sites. +- CDP commands: `Page.navigate`, `Page.captureScreenshot`, `Runtime.evaluate`, `Emulation.setDeviceMetricsOverride`. diff --git a/skills/cloudflare-browser/scripts/read-page.js b/skills/cloudflare-browser/scripts/read-page.js new file mode 100644 index 000000000..1c63cfe91 --- /dev/null +++ b/skills/cloudflare-browser/scripts/read-page.js @@ -0,0 +1,95 @@ +#!/usr/bin/env node +/** + * Read a web page via headless Chrome (Cloudflare Browser Rendering) + * + * Navigates to a URL, renders JavaScript, and extracts clean text. + * Works on JS-heavy/SPA sites that plain HTTP fetch can't read. + * + * Usage: + * node read-page.js URL [--max-chars 3000] [--html] [--wait 4000] + * + * Options: + * --max-chars N Max characters to extract (default: 3000) + * --html Output raw HTML instead of text + * --wait N Wait time in ms after navigation (default: 4000) + * + * Requires: CDP_SECRET, WORKER_URL environment variables + */ + +const { createClient } = require('./cdp-client'); + +async function main() { + var args = process.argv.slice(2); + var url = ''; + var maxChars = 3000; + var outputHtml = false; + var waitMs = 4000; + + for (var i = 0; i < args.length; i++) { + if (args[i] === '--max-chars' && args[i + 1]) { + maxChars = parseInt(args[i + 1], 10); + i++; + } else if (args[i] === '--html') { + outputHtml = true; + } else if (args[i] === '--wait' && args[i + 1]) { + waitMs = parseInt(args[i + 1], 10); + i++; + } else if (!url) { + url = args[i]; + } + } + + if (!url) { + console.error('Usage: node read-page.js URL [--max-chars 3000] [--html] [--wait 4000]'); + process.exit(1); + } + + var client; + try { + client = await createClient({ timeout: 30000 }); + await client.setViewport(1280, 800, 1, false); + await client.navigate(url, waitMs); + + if (outputHtml) { + var html = await client.getHTML(); + if (html) { + console.log(html.substring(0, maxChars)); + } + } else { + // Extract clean article text, stripping nav/footer/sidebar noise + var expression = 'JSON.stringify((function() {' + + 'var article = document.querySelector("article") || document.querySelector("[role=main]") || document.querySelector("main");' + + 'var el = article || document.body;' + + 'var clone = el.cloneNode(true);' + + 'var remove = clone.querySelectorAll("nav, footer, aside, header, script, style, [role=navigation], [role=banner], [role=complementary]");' + + 'for (var i = 0; i < remove.length; i++) remove[i].remove();' + + 'return clone.innerText.replace(/\\\\s+/g, " ").trim().substring(0, ' + maxChars + ');' + + '})())'; + + var result = await client.send('Runtime.evaluate', { + expression: expression, + returnByValue: true + }); + + if (result && result.result && result.result.value) { + var output = { + url: url, + timestamp: new Date().toISOString(), + charCount: JSON.parse(result.result.value).length, + content: JSON.parse(result.result.value) + }; + console.log(JSON.stringify(output, null, 2)); + } else { + console.error('[ERROR] Could not extract text from page'); + process.exit(1); + } + } + } catch (err) { + console.error('[ERROR] ' + err.message); + process.exit(1); + } finally { + if (client) client.close(); + } +} + +main(); diff --git a/skills/google-calendar/SKILL.md b/skills/google-calendar/SKILL.md new file mode 100644 index 000000000..ebbffb07b --- /dev/null +++ b/skills/google-calendar/SKILL.md @@ -0,0 +1,26 @@ +--- +name: google-calendar +description: Google Calendar management. List, create, search, update, delete events and check availability. +--- + +```bash +# List upcoming events (default 7 days) +node /root/clawd/skills/google-calendar/scripts/calendar.js list [--days 14] + +# Create event +node /root/clawd/skills/google-calendar/scripts/calendar.js create --title "Meeting" --start "2025-03-01T14:00" --end "2025-03-01T15:00" [--description "..."] [--attendees "a@b.com,c@d.com"] [--no-notify] + +# Search events +node /root/clawd/skills/google-calendar/scripts/calendar.js search --query "standup" + +# Check availability (yours + others) +node /root/clawd/skills/google-calendar/scripts/calendar.js freebusy --start "2025-03-01T09:00" --end "2025-03-01T18:00" [--emails "a@b.com,c@d.com"] + +# Update event +node /root/clawd/skills/google-calendar/scripts/calendar.js update --id EVENT_ID [--title "..."] [--start "..."] [--end "..."] [--description "..."] + +# Delete event +node /root/clawd/skills/google-calendar/scripts/calendar.js delete --id EVENT_ID +``` + +Auth is pre-configured (env vars already set). Just run the commands above. Times default to KST (Asia/Seoul). diff --git a/skills/google-calendar/scripts/calendar.js b/skills/google-calendar/scripts/calendar.js new file mode 100755 index 000000000..28c4fc2a2 --- /dev/null +++ b/skills/google-calendar/scripts/calendar.js @@ -0,0 +1,376 @@ +#!/usr/bin/env node +/** + * Google Calendar Skill - Manage calendar events via Google Calendar API v3 + * + * Usage: node calendar.js