From 851ed58a9324bab768fe5151546d87d6eca8f206 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 16 Mar 2026 18:02:34 -0400 Subject: [PATCH] chore: auto-commit uncommitted changes --- HEARTBEAT.md | 36 ++++++++--- memory/claude-usage.db | Bin 77824 -> 77824 bytes memory/claude-usage.json | 10 +-- memory/heartbeat-state.json | 6 +- memory/x-watch-last.md | 13 ++++ scripts/claude-usage-log.sh | 1 - scripts/k2-watchdog.sh | 3 +- scripts/notify.sh | 107 ++++++++++++++++++++++++++++++++ scripts/qwen-gguf-watch.sh | 19 +++--- scripts/signal-webhook-proxy.js | 67 -------------------- 10 files changed, 169 insertions(+), 93 deletions(-) create mode 100644 memory/x-watch-last.md create mode 100755 scripts/notify.sh delete mode 100644 scripts/signal-webhook-proxy.js diff --git a/HEARTBEAT.md b/HEARTBEAT.md index 08324db..3d79b20 100644 --- a/HEARTBEAT.md +++ b/HEARTBEAT.md @@ -130,7 +130,7 @@ Check and update Docker containers on 192.168.1.253 and HAOS on 192.168.1.252: - `docker compose pull` → `docker compose up -d` → `docker image prune -f` 3. Report what was updated in the weekly briefing -Services: immich, clickhouse, jellyfin, signal, qbittorrent-vpn +Services: immich, clickhouse, jellyfin, qbittorrent-vpn **qbittorrent-vpn: PULL ONLY, do NOT start.** Johan uses it on-demand. SSH: `ssh johan@192.168.1.253` @@ -245,13 +245,34 @@ Update `memory/heartbeat-state.json` with `lastTechScan` timestamp after running **State:** Track `lastIntraDayXScan` in `memory/heartbeat-state.json`. Skip if checked < 2h ago. ### ⚠️ ALWAYS SPAWN A SUBAGENT — never run inline -``` -sessions_spawn(task="Intra-day X scan: ...", label="x-watch") -``` X scanning = multiple bird calls + web searches = context pollution. Offload it. +### De-duplication (MANDATORY — do both before posting) + +**1. Last 24h only:** Only surface posts with timestamps within the last 24 hours. Discard anything older regardless of how interesting it is. + +**2. Check what was already posted:** Before posting anything to the dashboard or pinging Johan, fetch recent dashboard news: +```bash +curl -s http://localhost:9200/api/news | python3 -c "import json,sys; items=json.load(sys.stdin).get('news',[]); [print(i['title']) for i in items[:20]]" +``` +If a story with a similar title or topic was already posted today, **skip it**. Don't post NemoClaw twice. Don't post the same OC release three times. + +**3. Save what you surfaced:** After the scan, write a short summary of what was posted to `memory/x-watch-last.md` (overwrite each time): +``` +# Last X Watch: +- NemoClaw announced at GTC (steipete, NVIDIA) +- MiniMax M2.7 benchmarks circulating +``` +The next subagent reads this file at the start and skips anything already covered. + +### How to start each scan +1. Read `memory/x-watch-last.md` — know what was already covered +2. Fetch dashboard news (last 20 items) — know what's already posted +3. Run bird scans, filter to last 24h only +4. Only post what's genuinely new + ### Accounts to scan every run -Check recent posts (last ~4h) from each: +Check recent posts (last ~24h) from each: - **@Cloudflare** — MCP, Workers, AI integrations, platform announcements - **@openclaw** — releases, features, community highlights - **@steipete** — Peter Steinberger, OpenClaw creator @@ -288,9 +309,10 @@ Use: `bird user-tweets @handle` → filter for posts newer than last scan timest - Anything from @OpenAI/@MiniMax_AI/@Kimi_Moonshot/@ZhipuAI/@GeminiApp that isn't a model release, pricing change, or major product launch — these accounts post constantly, only hard news counts ### Subagent reports back with -- Any items surfaced (title + URL) -- "Nothing significant" if quiet +- Any items surfaced (title + URL) — new ones only, not repeats +- "Nothing new since last scan" if quiet - **Do NOT list accounts with no news** — not even in a "dropped" or "nothing from X" section. Only mention accounts that had something worth surfacing. +- Always write `memory/x-watch-last.md` even if nothing new — update the timestamp so the next scan knows when it last ran. --- diff --git a/memory/claude-usage.db b/memory/claude-usage.db index 97817800a09f46c838f88bf5323920a603fe1bd1..991506efc18bdc8031278353203aed24d6adc7e5 100644 GIT binary patch delta 382 zcmZp8z|!!5Wr8$g=0q81#>|Zg^Yt0iHyN-zV996M#V}c`;0_bZu8oaXm?wSatv3rX zGPE)#&F(xnYMH`22(~(Mr2pP z+$%04)0PI3=0K5#Ia*9cwk;JT&4?@wcexm^v}{`nraa93V%*ZQ8g0p#3Siz4bT1Zo950Fx?k)UknJ1d{U&w@DQN oy9xsj4kecdAORn&2; exit 1 ;; + esac +done +shift $((OPTIND - 1)) + +MESSAGE="${1:-}" +if [ -z "$MESSAGE" ]; then + echo "Usage: notify.sh [OPTIONS] \"message\"" >&2 + exit 1 +fi + +if [ "$URGENT" -eq 1 ]; then + PRIORITY=5 + TAGS="rotating_light" +fi + +# ── Send to ntfy ────────────────────────────────────────────────────────────── +ntfy_send() { + local topic="$1" + curl -s "$NTFY_URL/$topic" \ + -H "Authorization: Bearer $NTFY_TOKEN" \ + -H "Title: $TITLE" \ + -H "Priority: $PRIORITY" \ + -H "Tags: $TAGS" \ + -H "Markdown: yes" \ + -d "$MESSAGE" \ + > /dev/null +} + +# ── Send to dashboard news ──────────────────────────────────────────────────── +dashboard_send() { + # Map priority to dashboard type + local type="info" + [ "$PRIORITY" -ge 4 ] && type="warning" + [ "$PRIORITY" -ge 5 ] && type="error" + + curl -s -X POST "$DASHBOARD_URL" \ + -H "Content-Type: application/json" \ + -d "{\"title\":\"$TITLE\",\"body\":\"$MESSAGE\",\"type\":\"$type\",\"source\":\"notify\"}" \ + > /dev/null +} + +# ── Dispatch ────────────────────────────────────────────────────────────────── +case "$CHANNEL" in + forge) + ntfy_send "$TOPIC_FORGE" + ;; + inou) + ntfy_send "$TOPIC_INOU" + ;; + dashboard) + dashboard_send + ;; + all) + ntfy_send "$TOPIC_FORGE" + ntfy_send "$TOPIC_INOU" + dashboard_send + ;; + *) + echo "Unknown channel: $CHANNEL (forge|inou|dashboard|all)" >&2 + exit 1 + ;; +esac + +exit 0 diff --git a/scripts/qwen-gguf-watch.sh b/scripts/qwen-gguf-watch.sh index 6a912c6..1513e1a 100755 --- a/scripts/qwen-gguf-watch.sh +++ b/scripts/qwen-gguf-watch.sh @@ -16,15 +16,18 @@ for model in "${MODELS[@]}"; do model_short=$(basename "$model") echo "FOUND: $model" - # Post to dashboard - curl -s -X POST http://localhost:9200/api/news \ - -H 'Content-Type: application/json' \ - -d "{\"title\":\"GGUF Available: $model_short\",\"body\":\"$model is now available for download on HuggingFace.\",\"type\":\"success\",\"source\":\"qwen-gguf-watch\"}" \ - > /dev/null + # Post to dashboard + notify + /home/johan/clawd/scripts/notify.sh -c dashboard -t "GGUF Available: $model_short" -T "package" "$model is now available for download on HuggingFace." + /home/johan/clawd/scripts/notify.sh -t "GGUF ready: $model_short" -T "package" -p 3 "https://huggingface.co/$model" - # Signal Johan - curl -s -X POST "http://localhost:8080/api/v1/rpc" \ - -H "Content-Type: application/json" \ + FOUND=$((FOUND + 1)) + fi +done + +if [ $FOUND -eq 0 ]; then + echo "No Qwen3.5 GGUFs yet." +fi +cation/json" \ -d "{\"jsonrpc\":\"2.0\",\"method\":\"send\",\"params\":{\"recipient\":[\"+17272252475\"],\"message\":\"⚡ GGUF ready: $model_short — https://huggingface.co/$model\"},\"id\":1}" \ > /dev/null diff --git a/scripts/signal-webhook-proxy.js b/scripts/signal-webhook-proxy.js deleted file mode 100644 index e45a944..0000000 --- a/scripts/signal-webhook-proxy.js +++ /dev/null @@ -1,67 +0,0 @@ -#!/usr/bin/env node -/** - * Simple webhook proxy for Uptime Kuma → Signal - * Translates Uptime Kuma webhook payloads to signal-cli JSON-RPC - * - * Run: node signal-webhook-proxy.js - * Listens on port 8085 - */ - -const http = require('http'); - -const SIGNAL_RPC_URL = 'http://localhost:8080/api/v1/rpc'; -const RECIPIENT = '+31634481877'; -const PORT = 8085; - -async function sendSignal(message) { - const payload = { - jsonrpc: '2.0', - method: 'send', - params: { recipient: RECIPIENT, message }, - id: Date.now() - }; - - const res = await fetch(SIGNAL_RPC_URL, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(payload) - }); - return res.json(); -} - -const server = http.createServer(async (req, res) => { - if (req.method !== 'POST') { - res.writeHead(405); - res.end('Method not allowed'); - return; - } - - let body = ''; - req.on('data', chunk => body += chunk); - req.on('end', async () => { - try { - const data = JSON.parse(body); - - // Extract message from Uptime Kuma payload - // Uptime Kuma sends: { msg, monitor: { name }, heartbeat: { status, msg } } - const message = data.msg || - `[${data.monitor?.name || 'Unknown'}] ${data.heartbeat?.status === 1 ? '🟢 UP' : '🔴 DOWN'}`; - - console.log(`[${new Date().toISOString()}] Forwarding to Signal: ${message.substring(0, 100)}...`); - - const result = await sendSignal(message); - - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ ok: true, result })); - } catch (err) { - console.error('Error:', err.message); - res.writeHead(500, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ ok: false, error: err.message })); - } - }); -}); - -server.listen(PORT, '0.0.0.0', () => { - console.log(`Signal webhook proxy listening on http://0.0.0.0:${PORT}`); - console.log(`Configure Uptime Kuma webhook URL: http://james:${PORT}/`); -});