From f04a8955aa0b90c70a0408661c0a387c76eb07e9 Mon Sep 17 00:00:00 2001 From: Koshkoshinsk Date: Thu, 19 Mar 2026 15:05:24 +0000 Subject: [PATCH 01/14] feat: add opt-in diagnostics via PostHog Per-event consent diagnostics that sends anonymous install/update/skill data to PostHog. Conflict filenames are gated against upstream. Supports --dry-run to show exact payload before sending, and "never ask again" opt-out via state.yaml. Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/skills/_shared/diagnostics.md | 99 ++++++ .claude/skills/add-compact/SKILL.md | 5 + .claude/skills/add-discord/SKILL.md | 5 + .claude/skills/add-gmail/SKILL.md | 5 + .claude/skills/add-image-vision/SKILL.md | 5 + .claude/skills/add-ollama-tool/SKILL.md | 5 + .claude/skills/add-parallel/SKILL.md | 5 + .claude/skills/add-pdf-reader/SKILL.md | 5 + .claude/skills/add-reactions/SKILL.md | 5 + .claude/skills/add-slack/SKILL.md | 5 + .claude/skills/add-telegram-swarm/SKILL.md | 5 + .claude/skills/add-telegram/SKILL.md | 5 + .../skills/add-voice-transcription/SKILL.md | 5 + .claude/skills/add-whatsapp/SKILL.md | 5 + .../convert-to-apple-container/SKILL.md | 5 + .claude/skills/customize/SKILL.md | 5 + .claude/skills/debug/SKILL.md | 5 + .claude/skills/get-qodo-rules/SKILL.md | 5 + .claude/skills/qodo-pr-resolver/SKILL.md | 5 + .claude/skills/setup/SKILL.md | 5 + .claude/skills/update-nanoclaw/SKILL.md | 5 + .claude/skills/update-skills/SKILL.md | 5 + .claude/skills/use-local-whisper/SKILL.md | 5 + .claude/skills/x-integration/SKILL.md | 6 +- scripts/send-diagnostics.ts | 310 ++++++++++++++++++ 25 files changed, 524 insertions(+), 1 deletion(-) create mode 100644 .claude/skills/_shared/diagnostics.md create mode 100644 scripts/send-diagnostics.ts diff --git a/.claude/skills/_shared/diagnostics.md b/.claude/skills/_shared/diagnostics.md new file mode 100644 index 0000000..c509a70 --- /dev/null +++ b/.claude/skills/_shared/diagnostics.md @@ -0,0 +1,99 @@ +# Diagnostics (end-of-skill instructions) + +Follow these steps **after all other skill steps are complete**. + +## 1. Skip check + +If this skill was invoked from within another skill (e.g., `/add-whatsapp` called during `/setup`), **skip diagnostics entirely** — the parent skill handles it. Return without doing anything below. + +## 2. Determine event type + +- `/setup` → `setup_complete` +- `/update-nanoclaw` → `update_complete` +- Any other skill → `skill_applied` + +## 3. Build event-specific data + +Estimate `error_count` from the conversation (how many errors/retries occurred during the run). + +**For `setup_complete`:** +```json +{ + "channels_selected": ["whatsapp", "telegram"], + "error_count": 0, + "failed_step": null, + "exit_code": null +} +``` +- `channels_selected`: which channels the user chose to install +- `failed_step`: if setup failed, which step (environment / container / groups / register / mounts / service / verify) +- `exit_code`: if a step failed, its exit code + +**For `skill_applied`:** +```json +{ + "skill_name": "add-telegram", + "is_upstream_skill": true, + "conflict_files": ["package.json", "src/index.ts"], + "error_count": 0 +} +``` +- `skill_name`: the skill that was run. Use the upstream skill name if it's an upstream skill, otherwise use `"custom"` +- `is_upstream_skill`: true if this is a known upstream skill +- `conflict_files`: filenames that had merge conflicts (the script will gate these against upstream — pass them all, the script filters) + +**For `update_complete`:** +```json +{ + "version_age_days": 45, + "update_method": "merge", + "conflict_files": ["package.json"], + "breaking_changes_found": false, + "breaking_changes_skills_run": [], + "error_count": 0 +} +``` +- `version_age_days`: estimate from the backup tag or commit date how many days old the previous version was +- `update_method`: "merge" or "rebase" +- `breaking_changes_found`: whether breaking changes were detected during the update +- `breaking_changes_skills_run`: which skills had to be re-run to fix breaking changes + +## 4. Dry run + +Run with `--dry-run` to get the full payload: + +```bash +npx tsx scripts/send-diagnostics.ts --event --success --data '' --dry-run +``` + +Use `--failure` instead of `--success` if the skill failed. + +If the command produces no output, the user has opted out permanently — skip the rest. + +## 5. Show the user and ask + +Show the JSON output and ask: + +> "Would you like to send anonymous diagnostics to help improve NanoClaw? Here's exactly what would be sent:" +> +> (show the JSON) +> +> **Yes** / **No** / **Never ask again** + +Use AskUserQuestion. + +## 6. Handle response + +- **Yes**: Run the same command without `--dry-run`: + ```bash + npx tsx scripts/send-diagnostics.ts --event --success --data '' + ``` + Confirm: "Diagnostics sent." + +- **No**: Do nothing. User will be asked again next time. + +- **Never ask again**: Run: + ```bash + npx tsx -e "import { setNeverAsk } from './scripts/send-diagnostics.ts'; setNeverAsk();" + ``` + Confirm: "Got it — you won't be asked again." diff --git a/.claude/skills/add-compact/SKILL.md b/.claude/skills/add-compact/SKILL.md index 0c46165..fe7ca8a 100644 --- a/.claude/skills/add-compact/SKILL.md +++ b/.claude/skills/add-compact/SKILL.md @@ -133,3 +133,8 @@ npm test - **"Session commands require admin access"**: Only the device owner (`is_from_me`) or main-group senders can use `/compact`. Other users are denied. - **No compact_boundary in logs**: The SDK may not emit this event in all versions. Check the agent-runner logs for the warning message. Compaction may still have succeeded. - **Pre-compact failure**: If messages before `/compact` fail to process, the error message says "Failed to process messages before /compact." The cursor advances past sent output to prevent duplicates; `/compact` remains pending for the next attempt. + + +## Diagnostics (Optional) + +After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/add-discord/SKILL.md b/.claude/skills/add-discord/SKILL.md index e46bd3e..f4e98aa 100644 --- a/.claude/skills/add-discord/SKILL.md +++ b/.claude/skills/add-discord/SKILL.md @@ -201,3 +201,8 @@ The Discord bot supports: - @mention translation (Discord `<@botId>` → NanoClaw trigger format) - Message splitting for responses over 2000 characters - Typing indicators while the agent processes + + +## Diagnostics (Optional) + +After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/add-gmail/SKILL.md b/.claude/skills/add-gmail/SKILL.md index 781a0eb..b51a098 100644 --- a/.claude/skills/add-gmail/SKILL.md +++ b/.claude/skills/add-gmail/SKILL.md @@ -218,3 +218,8 @@ npx -y @gongrzhe/server-gmail-autoauth-mcp 6. Rebuild and restart 7. Clear stale agent-runner copies: `rm -r data/sessions/*/agent-runner-src 2>/dev/null || true` 8. Rebuild: `cd container && ./build.sh && cd .. && npm run build && launchctl kickstart -k gui/$(id -u)/com.nanoclaw` (macOS) or `systemctl --user restart nanoclaw` (Linux) + + +## Diagnostics (Optional) + +After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/add-image-vision/SKILL.md b/.claude/skills/add-image-vision/SKILL.md index 072bf7b..d42e394 100644 --- a/.claude/skills/add-image-vision/SKILL.md +++ b/.claude/skills/add-image-vision/SKILL.md @@ -92,3 +92,8 @@ All tests must pass and build must be clean before proceeding. - **"Image - download failed"**: Check WhatsApp connection stability. The download may timeout on slow connections. - **"Image - processing failed"**: Sharp may not be installed correctly. Run `npm ls sharp` to verify. - **Agent doesn't mention image content**: Check container logs for "Loaded image" messages. If missing, ensure agent-runner source was synced to group caches. + + +## Diagnostics (Optional) + +After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/add-ollama-tool/SKILL.md b/.claude/skills/add-ollama-tool/SKILL.md index a347b49..a28b8ea 100644 --- a/.claude/skills/add-ollama-tool/SKILL.md +++ b/.claude/skills/add-ollama-tool/SKILL.md @@ -151,3 +151,8 @@ The agent is trying to run `ollama` CLI inside the container instead of using th ### Agent doesn't use Ollama tools The agent may not know about the tools. Try being explicit: "use the ollama_generate tool with gemma3:1b to answer: ..." + + +## Diagnostics (Optional) + +After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/add-parallel/SKILL.md b/.claude/skills/add-parallel/SKILL.md index f4c1982..12eb58c 100644 --- a/.claude/skills/add-parallel/SKILL.md +++ b/.claude/skills/add-parallel/SKILL.md @@ -288,3 +288,8 @@ To remove Parallel AI integration: 3. Remove Web Research Tools section from groups/main/CLAUDE.md 4. Rebuild: `./container/build.sh && npm run build` 5. Restart: `launchctl kickstart -k gui/$(id -u)/com.nanoclaw` (macOS) or `systemctl --user restart nanoclaw` (Linux) + + +## Diagnostics (Optional) + +After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/add-pdf-reader/SKILL.md b/.claude/skills/add-pdf-reader/SKILL.md index a01e530..960d7fb 100644 --- a/.claude/skills/add-pdf-reader/SKILL.md +++ b/.claude/skills/add-pdf-reader/SKILL.md @@ -102,3 +102,8 @@ The PDF may be scanned (image-based). pdftotext only handles text-based PDFs. Co ### WhatsApp PDF not detected Verify the message has `documentMessage` with `mimetype: application/pdf`. Some file-sharing apps send PDFs as generic files without the correct mimetype. + + +## Diagnostics (Optional) + +After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/add-reactions/SKILL.md b/.claude/skills/add-reactions/SKILL.md index de86768..9eacebd 100644 --- a/.claude/skills/add-reactions/SKILL.md +++ b/.claude/skills/add-reactions/SKILL.md @@ -115,3 +115,8 @@ Ask the agent to react to a message via the `react_to_message` MCP tool. Check y - Check IPC logs for `Unauthorized IPC reaction attempt blocked` — the agent can only react in its own group's chat - Verify WhatsApp is connected: check logs for connection status + + +## Diagnostics (Optional) + +After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/add-slack/SKILL.md b/.claude/skills/add-slack/SKILL.md index 4c86e19..32a2cf0 100644 --- a/.claude/skills/add-slack/SKILL.md +++ b/.claude/skills/add-slack/SKILL.md @@ -205,3 +205,8 @@ The Slack channel supports: - **No file/image handling** — The bot only processes text content. File uploads, images, and rich message blocks are not forwarded to the agent. - **Channel metadata sync is unbounded** — `syncChannelMetadata()` paginates through all channels the bot is a member of, but has no upper bound or timeout. Workspaces with thousands of channels may experience slow startup. - **Workspace admin policies not detected** — If the Slack workspace restricts bot app installation, the setup will fail at the "Install to Workspace" step with no programmatic detection or guidance. See SLACK_SETUP.md troubleshooting section. + + +## Diagnostics (Optional) + +After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/add-telegram-swarm/SKILL.md b/.claude/skills/add-telegram-swarm/SKILL.md index ac4922c..b6e5923 100644 --- a/.claude/skills/add-telegram-swarm/SKILL.md +++ b/.claude/skills/add-telegram-swarm/SKILL.md @@ -382,3 +382,8 @@ To remove Agent Swarm support while keeping basic Telegram: 6. Remove Agent Teams section from group CLAUDE.md files 7. Remove `TELEGRAM_BOT_POOL` from `.env`, `data/env/env`, and launchd plist/systemd unit 8. Rebuild: `npm run build && ./container/build.sh && launchctl unload ~/Library/LaunchAgents/com.nanoclaw.plist && launchctl load ~/Library/LaunchAgents/com.nanoclaw.plist` (macOS) or `npm run build && ./container/build.sh && systemctl --user restart nanoclaw` (Linux) + + +## Diagnostics (Optional) + +After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/add-telegram/SKILL.md b/.claude/skills/add-telegram/SKILL.md index 10f25ab..86a137f 100644 --- a/.claude/skills/add-telegram/SKILL.md +++ b/.claude/skills/add-telegram/SKILL.md @@ -220,3 +220,8 @@ To remove Telegram integration: 4. Remove Telegram registrations from SQLite: `sqlite3 store/messages.db "DELETE FROM registered_groups WHERE jid LIKE 'tg:%'"` 5. Uninstall: `npm uninstall grammy` 6. Rebuild: `npm run build && launchctl kickstart -k gui/$(id -u)/com.nanoclaw` (macOS) or `npm run build && systemctl --user restart nanoclaw` (Linux) + + +## Diagnostics (Optional) + +After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/add-voice-transcription/SKILL.md b/.claude/skills/add-voice-transcription/SKILL.md index 8ccec32..d9f44b6 100644 --- a/.claude/skills/add-voice-transcription/SKILL.md +++ b/.claude/skills/add-voice-transcription/SKILL.md @@ -146,3 +146,8 @@ Check logs for the specific error. Common causes: ### Agent doesn't respond to voice notes Verify the chat is registered and the agent is running. Voice transcription only runs for registered groups. + + +## Diagnostics (Optional) + +After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/add-whatsapp/SKILL.md b/.claude/skills/add-whatsapp/SKILL.md index 0774799..c22a835 100644 --- a/.claude/skills/add-whatsapp/SKILL.md +++ b/.claude/skills/add-whatsapp/SKILL.md @@ -370,3 +370,8 @@ To remove WhatsApp integration: 2. Remove WhatsApp registrations: `sqlite3 store/messages.db "DELETE FROM registered_groups WHERE jid LIKE '%@g.us' OR jid LIKE '%@s.whatsapp.net'"` 3. Sync env: `mkdir -p data/env && cp .env data/env/env` 4. Rebuild and restart: `npm run build && launchctl kickstart -k gui/$(id -u)/com.nanoclaw` (macOS) or `npm run build && systemctl --user restart nanoclaw` (Linux) + + +## Diagnostics (Optional) + +After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/convert-to-apple-container/SKILL.md b/.claude/skills/convert-to-apple-container/SKILL.md index caf9c22..bcd1929 100644 --- a/.claude/skills/convert-to-apple-container/SKILL.md +++ b/.claude/skills/convert-to-apple-container/SKILL.md @@ -173,3 +173,8 @@ Check directory permissions on the host. The container runs as uid 1000. | `src/container-runner.ts` | .env shadow mount removed, main containers start as root with privilege drop | | `container/Dockerfile` | Entrypoint: `mount --bind` for .env shadowing, `setpriv` privilege drop | | `container/build.sh` | Default runtime: `docker` → `container` | + + +## Diagnostics (Optional) + +After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/customize/SKILL.md b/.claude/skills/customize/SKILL.md index 614a979..310f1ed 100644 --- a/.claude/skills/customize/SKILL.md +++ b/.claude/skills/customize/SKILL.md @@ -108,3 +108,8 @@ User: "Add Telegram as an input channel" 3. Create `src/channels/telegram.ts` implementing the `Channel` interface (see `src/channels/whatsapp.ts`) 4. Add the channel to `main()` in `src/index.ts` 5. Tell user how to authenticate and test + + +## Diagnostics (Optional) + +After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/debug/SKILL.md b/.claude/skills/debug/SKILL.md index 03c34de..e0fc3c7 100644 --- a/.claude/skills/debug/SKILL.md +++ b/.claude/skills/debug/SKILL.md @@ -347,3 +347,8 @@ echo -e "\n8. Session continuity working?" SESSIONS=$(grep "Session initialized" logs/nanoclaw.log 2>/dev/null | tail -5 | awk '{print $NF}' | sort -u | wc -l) [ "$SESSIONS" -le 2 ] && echo "OK (recent sessions reusing IDs)" || echo "CHECK - multiple different session IDs, may indicate resumption issues" ``` + + +## Diagnostics (Optional) + +After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/get-qodo-rules/SKILL.md b/.claude/skills/get-qodo-rules/SKILL.md index 69abaf7..4a2cf16 100644 --- a/.claude/skills/get-qodo-rules/SKILL.md +++ b/.claude/skills/get-qodo-rules/SKILL.md @@ -120,3 +120,8 @@ See `~/.qodo/config.json` for API key setup. Set `QODO_ENVIRONMENT_NAME` env var - **Not in git repo** - Inform the user that a git repository is required and exit gracefully; do not attempt code generation - **No API key** - Inform the user with setup instructions; set `QODO_API_KEY` or create `~/.qodo/config.json` - **No rules found** - Inform the user; set up rules at app.qodo.ai + + +## Diagnostics (Optional) + +After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/qodo-pr-resolver/SKILL.md b/.claude/skills/qodo-pr-resolver/SKILL.md index c0cbe22..165bbe2 100644 --- a/.claude/skills/qodo-pr-resolver/SKILL.md +++ b/.claude/skills/qodo-pr-resolver/SKILL.md @@ -324,3 +324,8 @@ Use the inline comment ID preserved during deduplication (Step 3b) to reply dire See [providers.md § Reply to Inline Comments](./resources/providers.md#reply-to-inline-comments) for provider-specific commands and reply format. Keep replies short (one line). If a reply fails, log it and continue. + + +## Diagnostics (Optional) + +After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/setup/SKILL.md b/.claude/skills/setup/SKILL.md index d173927..170e303 100644 --- a/.claude/skills/setup/SKILL.md +++ b/.claude/skills/setup/SKILL.md @@ -216,3 +216,8 @@ Tell user to test: send a message in their registered chat. Show: `tail -f logs/ **Channel not connecting:** Verify the channel's credentials are set in `.env`. Channels auto-enable when their credentials are present. For WhatsApp: check `store/auth/creds.json` exists. For token-based channels: check token values in `.env`. Restart the service after any `.env` change. **Unload service:** macOS: `launchctl unload ~/Library/LaunchAgents/com.nanoclaw.plist` | Linux: `systemctl --user stop nanoclaw` + + +## Diagnostics (Optional) + +After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/update-nanoclaw/SKILL.md b/.claude/skills/update-nanoclaw/SKILL.md index b0b478c..5e93c6c 100644 --- a/.claude/skills/update-nanoclaw/SKILL.md +++ b/.claude/skills/update-nanoclaw/SKILL.md @@ -233,3 +233,8 @@ Tell the user: - Restart the service to apply changes: - If using launchd: `launchctl unload ~/Library/LaunchAgents/com.nanoclaw.plist && launchctl load ~/Library/LaunchAgents/com.nanoclaw.plist` - If running manually: restart `npm run dev` + + +## Diagnostics (Optional) + +After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/update-skills/SKILL.md b/.claude/skills/update-skills/SKILL.md index cbbff39..ade436b 100644 --- a/.claude/skills/update-skills/SKILL.md +++ b/.claude/skills/update-skills/SKILL.md @@ -128,3 +128,8 @@ Show: - Any conflicts that were resolved (list files) If the service is running, remind the user to restart it to pick up changes. + + +## Diagnostics (Optional) + +After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/use-local-whisper/SKILL.md b/.claude/skills/use-local-whisper/SKILL.md index ec18a09..b7d9814 100644 --- a/.claude/skills/use-local-whisper/SKILL.md +++ b/.claude/skills/use-local-whisper/SKILL.md @@ -150,3 +150,8 @@ whisper-cli -m data/models/ggml-base.bin -f /tmp/test.wav --no-timestamps -nt **Slow transcription**: The base model processes ~30s of audio in <1s on M1+. If slower, check CPU usage — another process may be competing. **Wrong language**: whisper.cpp auto-detects language. To force a language, you can set `WHISPER_LANG` and modify `src/transcription.ts` to pass `-l $WHISPER_LANG`. + + +## Diagnostics (Optional) + +After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/x-integration/SKILL.md b/.claude/skills/x-integration/SKILL.md index 29a7be6..86ae041 100644 --- a/.claude/skills/x-integration/SKILL.md +++ b/.claude/skills/x-integration/SKILL.md @@ -414,4 +414,8 @@ docker run nanoclaw-agent ls -la /app/src/skills/ - `data/x-browser-profile/` - Contains X session cookies (in `.gitignore`) - `data/x-auth.json` - Auth state marker (in `.gitignore`) - Only main group can use X tools (enforced in `agent.ts` and `host.ts`) -- Scripts run as subprocesses with limited environment \ No newline at end of file +- Scripts run as subprocesses with limited environment + +## Diagnostics (Optional) + +After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/scripts/send-diagnostics.ts b/scripts/send-diagnostics.ts new file mode 100644 index 0000000..157307c --- /dev/null +++ b/scripts/send-diagnostics.ts @@ -0,0 +1,310 @@ +/** + * send-diagnostics.ts — opt-in, privacy-first diagnostics for NanoClaw. + * + * Collects system info, accepts event-specific data via --data JSON arg, + * gates conflict filenames against upstream, and sends to PostHog. + * + * Usage: + * npx tsx scripts/send-diagnostics.ts \ + * --event \ + * [--success|--failure] \ + * [--data ''] \ + * [--dry-run] + * + * Never exits non-zero on telemetry failures. + */ + +import { execSync } from 'child_process'; +import crypto from 'crypto'; +import fs from 'fs'; +import path from 'path'; +import { parse as parseYaml, stringify as stringifyYaml } from 'yaml'; + +const POSTHOG_ENDPOINT = 'https://us.i.posthog.com/capture/'; +const POSTHOG_TOKEN = 'phc_fx1Hhx9ucz8GuaJC8LVZWO8u03yXZZJJ6ObS4yplnaP'; +const SEND_TIMEOUT_MS = 5000; + +const PROJECT_ROOT = path.resolve(import.meta.dirname, '..'); +const STATE_YAML_PATH = path.join(PROJECT_ROOT, '.nanoclaw', 'state.yaml'); + +// --- Args --- + +function parseArgs(): { + event: string; + success?: boolean; + data: Record; + dryRun: boolean; +} { + const args = process.argv.slice(2); + let event = ''; + let success: boolean | undefined; + let data: Record = {}; + let dryRun = false; + + for (let i = 0; i < args.length; i++) { + switch (args[i]) { + case '--event': + event = args[++i] || ''; + break; + case '--success': + success = true; + break; + case '--failure': + success = false; + break; + case '--data': + try { + data = JSON.parse(args[++i] || '{}'); + } catch { + console.error('Warning: --data JSON parse failed, ignoring'); + } + break; + case '--dry-run': + dryRun = true; + break; + } + } + + if (!event) { + console.error('Error: --event is required'); + process.exit(0); // exit 0 — never fail on diagnostics + } + + return { event, success, data, dryRun }; +} + +// --- State (neverAsk) --- + +function readState(): Record { + try { + const raw = fs.readFileSync(STATE_YAML_PATH, 'utf-8'); + return parseYaml(raw) || {}; + } catch { + return {}; + } +} + +function isNeverAsk(): boolean { + const state = readState(); + return state.neverAsk === true; +} + +export function setNeverAsk(): void { + const state = readState(); + state.neverAsk = true; + const dir = path.dirname(STATE_YAML_PATH); + fs.mkdirSync(dir, { recursive: true }); + fs.writeFileSync(STATE_YAML_PATH, stringifyYaml(state)); +} + +// --- Git helpers --- + +/** Resolve the upstream remote ref (could be 'upstream/main' or 'origin/main'). */ +function resolveUpstreamRef(): string | null { + for (const ref of ['upstream/main', 'origin/main']) { + try { + execSync(`git rev-parse --verify ${ref}`, { + cwd: PROJECT_ROOT, + stdio: 'ignore', + }); + return ref; + } catch { + continue; + } + } + return null; +} + +// --- System info --- + +function getNanoclawVersion(): string { + try { + const pkg = JSON.parse( + fs.readFileSync(path.join(PROJECT_ROOT, 'package.json'), 'utf-8'), + ); + return pkg.version || 'unknown'; + } catch { + return 'unknown'; + } +} + +function getNodeMajorVersion(): number | null { + const match = process.version.match(/^v(\d+)/); + return match ? parseInt(match[1], 10) : null; +} + +function getContainerRuntime(): string { + try { + const src = fs.readFileSync( + path.join(PROJECT_ROOT, 'src', 'container-runtime.ts'), + 'utf-8', + ); + const match = src.match(/CONTAINER_RUNTIME_BIN\s*=\s*['"]([^'"]+)['"]/); + return match ? match[1] : 'unknown'; + } catch { + return 'unknown'; + } +} + +function isUpstreamCommit(): boolean { + const ref = resolveUpstreamRef(); + if (!ref) return false; + try { + const head = execSync('git rev-parse HEAD', { + encoding: 'utf-8', + cwd: PROJECT_ROOT, + stdio: ['pipe', 'pipe', 'ignore'], + }).trim(); + execSync(`git merge-base --is-ancestor ${head} ${ref}`, { + cwd: PROJECT_ROOT, + stdio: 'ignore', + }); + return true; + } catch { + return false; + } +} + +function collectSystemInfo(): Record { + return { + nanoclaw_version: getNanoclawVersion(), + os_platform: process.platform, + arch: process.arch, + node_major_version: getNodeMajorVersion(), + container_runtime: getContainerRuntime(), + is_upstream_commit: isUpstreamCommit(), + }; +} + +// --- Conflict filename gating --- + +function getUpstreamFiles(): Set | null { + const ref = resolveUpstreamRef(); + if (!ref) return null; + try { + const output = execSync(`git ls-tree -r --name-only ${ref}`, { + encoding: 'utf-8', + cwd: PROJECT_ROOT, + stdio: ['pipe', 'pipe', 'ignore'], + }); + return new Set(output.trim().split('\n').filter(Boolean)); + } catch { + return null; + } +} + +function gateConflictFiles(data: Record): void { + if (!Array.isArray(data.conflict_files)) return; + + const rawFiles: string[] = data.conflict_files; + const upstreamFiles = getUpstreamFiles(); + const totalCount = rawFiles.length; + + if (!upstreamFiles) { + // Can't verify — fail-closed + data.conflict_files = []; + data.conflict_count = totalCount; + data.has_non_upstream_conflicts = totalCount > 0; + return; + } + + const safe: string[] = []; + let hasNonUpstream = false; + + for (const file of rawFiles) { + if (upstreamFiles.has(file)) { + safe.push(file); + } else { + hasNonUpstream = true; + } + } + + data.conflict_files = safe; + data.conflict_count = totalCount; + data.has_non_upstream_conflicts = hasNonUpstream; +} + +// --- Build & send --- + +function buildPayload( + event: string, + systemInfo: Record, + eventData: Record, + success?: boolean, +): Record { + const properties: Record = { + $process_person_profile: false, + $lib: 'nanoclaw-diagnostics', + ...systemInfo, + ...eventData, + }; + + if (success !== undefined) { + properties.success = success; + } + + return { + api_key: POSTHOG_TOKEN, + event, + distinct_id: crypto.randomUUID(), + properties, + }; +} + +async function sendToPostHog( + payload: Record, +): Promise { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), SEND_TIMEOUT_MS); + + try { + const response = await fetch(POSTHOG_ENDPOINT, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + signal: controller.signal, + }); + if (response.ok) { + console.log('Diagnostics sent successfully.'); + } else { + console.error( + `Diagnostics send failed (HTTP ${response.status}). This is fine.`, + ); + } + } catch (err) { + console.error('Diagnostics send failed (network error). This is fine.'); + } finally { + clearTimeout(timeout); + } +} + +// --- Main --- + +async function main(): Promise { + try { + if (isNeverAsk()) { + // User opted out permanently — exit silently + return; + } + + const { event, success, data, dryRun } = parseArgs(); + + // Gate conflict filenames before building payload + gateConflictFiles(data); + + const systemInfo = collectSystemInfo(); + const payload = buildPayload(event, systemInfo, data, success); + + if (dryRun) { + console.log(JSON.stringify(payload, null, 2)); + return; + } + + await sendToPostHog(payload); + } catch (err) { + // Never fail on diagnostics + console.error('Diagnostics error (this is fine):', (err as Error).message); + } +} + +main(); From 33874de17507359ccfa114a14fd268548c612dea Mon Sep 17 00:00:00 2001 From: Koshkoshinsk Date: Thu, 19 Mar 2026 21:17:55 +0000 Subject: [PATCH 02/14] fix: strip api_key from dry-run output shown to user Co-Authored-By: Claude Opus 4.6 (1M context) --- scripts/send-diagnostics.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/send-diagnostics.ts b/scripts/send-diagnostics.ts index 157307c..80d5124 100644 --- a/scripts/send-diagnostics.ts +++ b/scripts/send-diagnostics.ts @@ -296,7 +296,9 @@ async function main(): Promise { const payload = buildPayload(event, systemInfo, data, success); if (dryRun) { - console.log(JSON.stringify(payload, null, 2)); + // Strip secrets before showing to user + const { api_key, ...visible } = payload; + console.log(JSON.stringify(visible, null, 2)); return; } From 3747dfeacc104fd392a4cfecd3b7e65eb02cfe02 Mon Sep 17 00:00:00 2001 From: Koshkoshinsk Date: Thu, 19 Mar 2026 21:19:16 +0000 Subject: [PATCH 03/14] fix: also strip distinct_id from dry-run output Ephemeral UUID is harmless but showing it to users creates unnecessary doubt about whether they're being tracked. Co-Authored-By: Claude Opus 4.6 (1M context) --- scripts/send-diagnostics.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/send-diagnostics.ts b/scripts/send-diagnostics.ts index 80d5124..fe37585 100644 --- a/scripts/send-diagnostics.ts +++ b/scripts/send-diagnostics.ts @@ -296,8 +296,8 @@ async function main(): Promise { const payload = buildPayload(event, systemInfo, data, success); if (dryRun) { - // Strip secrets before showing to user - const { api_key, ...visible } = payload; + // Strip internal fields before showing to user + const { api_key, distinct_id, ...visible } = payload; console.log(JSON.stringify(visible, null, 2)); return; } From 8c1d5598bafdda58763a6cbaa6e1ad0822436e27 Mon Sep 17 00:00:00 2001 From: Koshkoshinsk Date: Thu, 19 Mar 2026 21:21:29 +0000 Subject: [PATCH 04/14] fix: strip PostHog internal fields from dry-run output Co-Authored-By: Claude Opus 4.6 (1M context) --- scripts/send-diagnostics.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scripts/send-diagnostics.ts b/scripts/send-diagnostics.ts index fe37585..5b5399c 100644 --- a/scripts/send-diagnostics.ts +++ b/scripts/send-diagnostics.ts @@ -298,6 +298,9 @@ async function main(): Promise { if (dryRun) { // Strip internal fields before showing to user const { api_key, distinct_id, ...visible } = payload; + const props = visible.properties as Record; + delete props.$process_person_profile; + delete props.$lib; console.log(JSON.stringify(visible, null, 2)); return; } From 1734be725990bd7d6cbd7fa94ce738082aeefcf4 Mon Sep 17 00:00:00 2001 From: NanoClaw User Date: Thu, 19 Mar 2026 22:20:11 +0000 Subject: [PATCH 05/14] fix: collect diagnostics for sub-skills invoked during setup Previously, sub-skills (e.g. /add-telegram) skipped diagnostics when called from a parent skill like /setup. This lost channel-level events. Now all events are collected and shown to the user in a single prompt. Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/skills/_shared/diagnostics.md | 31 ++++++++++++++++----------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/.claude/skills/_shared/diagnostics.md b/.claude/skills/_shared/diagnostics.md index c509a70..ac5994b 100644 --- a/.claude/skills/_shared/diagnostics.md +++ b/.claude/skills/_shared/diagnostics.md @@ -2,11 +2,18 @@ Follow these steps **after all other skill steps are complete**. -## 1. Skip check +## 1. Collect all events -If this skill was invoked from within another skill (e.g., `/add-whatsapp` called during `/setup`), **skip diagnostics entirely** — the parent skill handles it. Return without doing anything below. +Every skill sends its own diagnostics event — even when invoked from a parent skill. A `/setup` that invokes `/add-telegram` produces **two** events: `skill_applied` (for add-telegram) and `setup_complete` (for setup). A `/setup` with no channels produces just `setup_complete`. -## 2. Determine event type +Build the list of events to send: + +1. If sub-skills were invoked during this run (e.g., `/add-telegram` during `/setup`), add a `skill_applied` event for each. +2. Add the event for the current (outermost) skill. + +**If running a sub-skill standalone** (not called from a parent), there is just one event. + +## 2. Determine event type (for each event) - `/setup` → `setup_complete` - `/update-nanoclaw` → `update_complete` @@ -58,25 +65,25 @@ Estimate `error_count` from the conversation (how many errors/retries occurred d - `breaking_changes_found`: whether breaking changes were detected during the update - `breaking_changes_skills_run`: which skills had to be re-run to fix breaking changes -## 4. Dry run +## 4. Dry run all events -Run with `--dry-run` to get the full payload: +For **each** event, run with `--dry-run` to get the payload: ```bash npx tsx scripts/send-diagnostics.ts --event --success --data '' --dry-run ``` -Use `--failure` instead of `--success` if the skill failed. +Use `--failure` instead of `--success` if that particular skill/step failed. -If the command produces no output, the user has opted out permanently — skip the rest. +If **any** dry-run produces no output, the user has opted out permanently — skip the rest for all events. -## 5. Show the user and ask +## 5. Show the user and ask once -Show the JSON output and ask: +Show **all** payloads together and ask **once** (not per-event): > "Would you like to send anonymous diagnostics to help improve NanoClaw? Here's exactly what would be sent:" > -> (show the JSON) +> (show all JSON payloads) > > **Yes** / **No** / **Never ask again** @@ -84,11 +91,11 @@ Use AskUserQuestion. ## 6. Handle response -- **Yes**: Run the same command without `--dry-run`: +- **Yes**: Send **all** events (run each command without `--dry-run`): ```bash npx tsx scripts/send-diagnostics.ts --event --success --data '' ``` - Confirm: "Diagnostics sent." + Confirm: "Diagnostics sent (N events)." or "Diagnostics sent." if only one. - **No**: Do nothing. User will be asked again next time. From d96be5ddfd1f2c8d7817d7e3650d5f28c1b8d415 Mon Sep 17 00:00:00 2001 From: gavrielc Date: Sat, 21 Mar 2026 16:27:10 +0200 Subject: [PATCH 06/14] scope diagnostics to setup and update-nanoclaw only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove diagnostics appendage from all other skills. Only /setup and /update-nanoclaw need telemetry — these are the two points where we can detect regressions and track improvements across the user base. Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/skills/add-compact/SKILL.md | 5 - .claude/skills/add-discord/SKILL.md | 5 - .claude/skills/add-gmail/SKILL.md | 5 - .claude/skills/add-image-vision/SKILL.md | 5 - .claude/skills/add-ollama-tool/SKILL.md | 5 - .claude/skills/add-parallel/SKILL.md | 5 - .claude/skills/add-pdf-reader/SKILL.md | 5 - .claude/skills/add-reactions/SKILL.md | 5 - .claude/skills/add-slack/SKILL.md | 5 - .claude/skills/add-telegram-swarm/SKILL.md | 5 - .claude/skills/add-telegram/SKILL.md | 5 - .../skills/add-voice-transcription/SKILL.md | 5 - .claude/skills/add-whatsapp/SKILL.md | 5 - .claude/skills/claw/SKILL.md | 131 ++++++++++++++++++ .../convert-to-apple-container/SKILL.md | 5 - .claude/skills/customize/SKILL.md | 5 - .claude/skills/debug/SKILL.md | 5 - .claude/skills/get-qodo-rules/SKILL.md | 5 - .claude/skills/qodo-pr-resolver/SKILL.md | 5 - .claude/skills/update-skills/SKILL.md | 5 - .claude/skills/use-local-whisper/SKILL.md | 5 - .claude/skills/x-integration/SKILL.md | 6 +- 22 files changed, 132 insertions(+), 105 deletions(-) create mode 100644 .claude/skills/claw/SKILL.md diff --git a/.claude/skills/add-compact/SKILL.md b/.claude/skills/add-compact/SKILL.md index fe7ca8a..0c46165 100644 --- a/.claude/skills/add-compact/SKILL.md +++ b/.claude/skills/add-compact/SKILL.md @@ -133,8 +133,3 @@ npm test - **"Session commands require admin access"**: Only the device owner (`is_from_me`) or main-group senders can use `/compact`. Other users are denied. - **No compact_boundary in logs**: The SDK may not emit this event in all versions. Check the agent-runner logs for the warning message. Compaction may still have succeeded. - **Pre-compact failure**: If messages before `/compact` fail to process, the error message says "Failed to process messages before /compact." The cursor advances past sent output to prevent duplicates; `/compact` remains pending for the next attempt. - - -## Diagnostics (Optional) - -After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/add-discord/SKILL.md b/.claude/skills/add-discord/SKILL.md index f4e98aa..e46bd3e 100644 --- a/.claude/skills/add-discord/SKILL.md +++ b/.claude/skills/add-discord/SKILL.md @@ -201,8 +201,3 @@ The Discord bot supports: - @mention translation (Discord `<@botId>` → NanoClaw trigger format) - Message splitting for responses over 2000 characters - Typing indicators while the agent processes - - -## Diagnostics (Optional) - -After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/add-gmail/SKILL.md b/.claude/skills/add-gmail/SKILL.md index b51a098..781a0eb 100644 --- a/.claude/skills/add-gmail/SKILL.md +++ b/.claude/skills/add-gmail/SKILL.md @@ -218,8 +218,3 @@ npx -y @gongrzhe/server-gmail-autoauth-mcp 6. Rebuild and restart 7. Clear stale agent-runner copies: `rm -r data/sessions/*/agent-runner-src 2>/dev/null || true` 8. Rebuild: `cd container && ./build.sh && cd .. && npm run build && launchctl kickstart -k gui/$(id -u)/com.nanoclaw` (macOS) or `systemctl --user restart nanoclaw` (Linux) - - -## Diagnostics (Optional) - -After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/add-image-vision/SKILL.md b/.claude/skills/add-image-vision/SKILL.md index d42e394..072bf7b 100644 --- a/.claude/skills/add-image-vision/SKILL.md +++ b/.claude/skills/add-image-vision/SKILL.md @@ -92,8 +92,3 @@ All tests must pass and build must be clean before proceeding. - **"Image - download failed"**: Check WhatsApp connection stability. The download may timeout on slow connections. - **"Image - processing failed"**: Sharp may not be installed correctly. Run `npm ls sharp` to verify. - **Agent doesn't mention image content**: Check container logs for "Loaded image" messages. If missing, ensure agent-runner source was synced to group caches. - - -## Diagnostics (Optional) - -After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/add-ollama-tool/SKILL.md b/.claude/skills/add-ollama-tool/SKILL.md index a28b8ea..a347b49 100644 --- a/.claude/skills/add-ollama-tool/SKILL.md +++ b/.claude/skills/add-ollama-tool/SKILL.md @@ -151,8 +151,3 @@ The agent is trying to run `ollama` CLI inside the container instead of using th ### Agent doesn't use Ollama tools The agent may not know about the tools. Try being explicit: "use the ollama_generate tool with gemma3:1b to answer: ..." - - -## Diagnostics (Optional) - -After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/add-parallel/SKILL.md b/.claude/skills/add-parallel/SKILL.md index 12eb58c..f4c1982 100644 --- a/.claude/skills/add-parallel/SKILL.md +++ b/.claude/skills/add-parallel/SKILL.md @@ -288,8 +288,3 @@ To remove Parallel AI integration: 3. Remove Web Research Tools section from groups/main/CLAUDE.md 4. Rebuild: `./container/build.sh && npm run build` 5. Restart: `launchctl kickstart -k gui/$(id -u)/com.nanoclaw` (macOS) or `systemctl --user restart nanoclaw` (Linux) - - -## Diagnostics (Optional) - -After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/add-pdf-reader/SKILL.md b/.claude/skills/add-pdf-reader/SKILL.md index 960d7fb..a01e530 100644 --- a/.claude/skills/add-pdf-reader/SKILL.md +++ b/.claude/skills/add-pdf-reader/SKILL.md @@ -102,8 +102,3 @@ The PDF may be scanned (image-based). pdftotext only handles text-based PDFs. Co ### WhatsApp PDF not detected Verify the message has `documentMessage` with `mimetype: application/pdf`. Some file-sharing apps send PDFs as generic files without the correct mimetype. - - -## Diagnostics (Optional) - -After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/add-reactions/SKILL.md b/.claude/skills/add-reactions/SKILL.md index 9eacebd..de86768 100644 --- a/.claude/skills/add-reactions/SKILL.md +++ b/.claude/skills/add-reactions/SKILL.md @@ -115,8 +115,3 @@ Ask the agent to react to a message via the `react_to_message` MCP tool. Check y - Check IPC logs for `Unauthorized IPC reaction attempt blocked` — the agent can only react in its own group's chat - Verify WhatsApp is connected: check logs for connection status - - -## Diagnostics (Optional) - -After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/add-slack/SKILL.md b/.claude/skills/add-slack/SKILL.md index 32a2cf0..4c86e19 100644 --- a/.claude/skills/add-slack/SKILL.md +++ b/.claude/skills/add-slack/SKILL.md @@ -205,8 +205,3 @@ The Slack channel supports: - **No file/image handling** — The bot only processes text content. File uploads, images, and rich message blocks are not forwarded to the agent. - **Channel metadata sync is unbounded** — `syncChannelMetadata()` paginates through all channels the bot is a member of, but has no upper bound or timeout. Workspaces with thousands of channels may experience slow startup. - **Workspace admin policies not detected** — If the Slack workspace restricts bot app installation, the setup will fail at the "Install to Workspace" step with no programmatic detection or guidance. See SLACK_SETUP.md troubleshooting section. - - -## Diagnostics (Optional) - -After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/add-telegram-swarm/SKILL.md b/.claude/skills/add-telegram-swarm/SKILL.md index b6e5923..ac4922c 100644 --- a/.claude/skills/add-telegram-swarm/SKILL.md +++ b/.claude/skills/add-telegram-swarm/SKILL.md @@ -382,8 +382,3 @@ To remove Agent Swarm support while keeping basic Telegram: 6. Remove Agent Teams section from group CLAUDE.md files 7. Remove `TELEGRAM_BOT_POOL` from `.env`, `data/env/env`, and launchd plist/systemd unit 8. Rebuild: `npm run build && ./container/build.sh && launchctl unload ~/Library/LaunchAgents/com.nanoclaw.plist && launchctl load ~/Library/LaunchAgents/com.nanoclaw.plist` (macOS) or `npm run build && ./container/build.sh && systemctl --user restart nanoclaw` (Linux) - - -## Diagnostics (Optional) - -After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/add-telegram/SKILL.md b/.claude/skills/add-telegram/SKILL.md index 86a137f..10f25ab 100644 --- a/.claude/skills/add-telegram/SKILL.md +++ b/.claude/skills/add-telegram/SKILL.md @@ -220,8 +220,3 @@ To remove Telegram integration: 4. Remove Telegram registrations from SQLite: `sqlite3 store/messages.db "DELETE FROM registered_groups WHERE jid LIKE 'tg:%'"` 5. Uninstall: `npm uninstall grammy` 6. Rebuild: `npm run build && launchctl kickstart -k gui/$(id -u)/com.nanoclaw` (macOS) or `npm run build && systemctl --user restart nanoclaw` (Linux) - - -## Diagnostics (Optional) - -After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/add-voice-transcription/SKILL.md b/.claude/skills/add-voice-transcription/SKILL.md index d9f44b6..8ccec32 100644 --- a/.claude/skills/add-voice-transcription/SKILL.md +++ b/.claude/skills/add-voice-transcription/SKILL.md @@ -146,8 +146,3 @@ Check logs for the specific error. Common causes: ### Agent doesn't respond to voice notes Verify the chat is registered and the agent is running. Voice transcription only runs for registered groups. - - -## Diagnostics (Optional) - -After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/add-whatsapp/SKILL.md b/.claude/skills/add-whatsapp/SKILL.md index c22a835..0774799 100644 --- a/.claude/skills/add-whatsapp/SKILL.md +++ b/.claude/skills/add-whatsapp/SKILL.md @@ -370,8 +370,3 @@ To remove WhatsApp integration: 2. Remove WhatsApp registrations: `sqlite3 store/messages.db "DELETE FROM registered_groups WHERE jid LIKE '%@g.us' OR jid LIKE '%@s.whatsapp.net'"` 3. Sync env: `mkdir -p data/env && cp .env data/env/env` 4. Rebuild and restart: `npm run build && launchctl kickstart -k gui/$(id -u)/com.nanoclaw` (macOS) or `npm run build && systemctl --user restart nanoclaw` (Linux) - - -## Diagnostics (Optional) - -After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/claw/SKILL.md b/.claude/skills/claw/SKILL.md new file mode 100644 index 0000000..10e0dc3 --- /dev/null +++ b/.claude/skills/claw/SKILL.md @@ -0,0 +1,131 @@ +--- +name: claw +description: Install the claw CLI tool — run NanoClaw agent containers from the command line without opening a chat app. +--- + +# claw — NanoClaw CLI + +`claw` is a Python CLI that sends prompts directly to a NanoClaw agent container from the terminal. It reads registered groups from the NanoClaw database, picks up secrets from `.env`, and pipes a JSON payload into a container run — no chat app required. + +## What it does + +- Send a prompt to any registered group by name, folder, or JID +- Default target is the main group (no `-g` needed for most use) +- Resume a previous session with `-s ` +- Read prompts from stdin (`--pipe`) for scripting and piping +- List all registered groups with `--list-groups` +- Auto-detects `container` or `docker` runtime (or override with `--runtime`) +- Prints the agent's response to stdout; session ID to stderr +- Verbose mode (`-v`) shows the command, redacted payload, and exit code + +## Prerequisites + +- Python 3.8 or later +- NanoClaw installed with a built and tagged container image (`nanoclaw-agent:latest`) +- Either `container` (Apple Container, macOS 15+) or `docker` available in `PATH` + +## Install + +Run this skill from within the NanoClaw directory. The script auto-detects its location, so the symlink always points to the right place. + +### 1. Copy the script + +```bash +mkdir -p scripts +cp "${CLAUDE_SKILL_DIR}/scripts/claw" scripts/claw +chmod +x scripts/claw +``` + +### 2. Symlink into PATH + +```bash +mkdir -p ~/bin +ln -sf "$(pwd)/scripts/claw" ~/bin/claw +``` + +Make sure `~/bin` is in `PATH`. Add this to `~/.zshrc` or `~/.bashrc` if needed: + +```bash +export PATH="$HOME/bin:$PATH" +``` + +Then reload the shell: + +```bash +source ~/.zshrc # or ~/.bashrc +``` + +### 3. Verify + +```bash +claw --list-groups +``` + +You should see registered groups. If NanoClaw isn't running or the database doesn't exist yet, the list will be empty — that's fine. + +## Usage Examples + +```bash +# Send a prompt to the main group +claw "What's on my calendar today?" + +# Send to a specific group by name (fuzzy match) +claw -g "family" "Remind everyone about dinner at 7" + +# Send to a group by exact JID +claw -j "120363336345536173@g.us" "Hello" + +# Resume a previous session +claw -s abc123 "Continue where we left off" + +# Read prompt from stdin +echo "Summarize this" | claw --pipe -g dev + +# Pipe a file +cat report.txt | claw --pipe "Summarize this report" + +# List all registered groups +claw --list-groups + +# Force a specific runtime +claw --runtime docker "Hello" + +# Use a custom image tag (e.g. after rebuilding with a new tag) +claw --image nanoclaw-agent:dev "Hello" + +# Verbose mode (debug info, secrets redacted) +claw -v "Hello" + +# Custom timeout for long-running tasks +claw --timeout 600 "Run the full analysis" +``` + +## Troubleshooting + +### "neither 'container' nor 'docker' found" + +Install Docker Desktop or Apple Container (macOS 15+), or pass `--runtime` explicitly. + +### "no secrets found in .env" + +The script auto-detects your NanoClaw directory and reads `.env` from it. Check that the file exists and contains at least one of: `CLAUDE_CODE_OAUTH_TOKEN`, `ANTHROPIC_API_KEY`, `ANTHROPIC_AUTH_TOKEN`. + +### Container times out + +The default timeout is 300 seconds. For longer tasks, pass `--timeout 600` (or higher). If the container consistently hangs, check that your `nanoclaw-agent:latest` image is up to date by running `./container/build.sh`. + +### "group not found" + +Run `claw --list-groups` to see what's registered. Group lookup does a fuzzy partial match on name and folder — if your query matches multiple groups, you'll get an error listing the ambiguous matches. + +### Container crashes mid-stream + +Containers run with `--rm` so they are automatically removed. If the agent crashes before emitting the output sentinel, `claw` falls back to printing raw stdout. Use `-v` to see what the container produced. Rebuild the image with `./container/build.sh` if crashes are consistent. + +### Override the NanoClaw directory + +If `claw` can't find your database or `.env`, set the `NANOCLAW_DIR` environment variable: + +```bash +export NANOCLAW_DIR=/path/to/your/nanoclaw +``` diff --git a/.claude/skills/convert-to-apple-container/SKILL.md b/.claude/skills/convert-to-apple-container/SKILL.md index bcd1929..caf9c22 100644 --- a/.claude/skills/convert-to-apple-container/SKILL.md +++ b/.claude/skills/convert-to-apple-container/SKILL.md @@ -173,8 +173,3 @@ Check directory permissions on the host. The container runs as uid 1000. | `src/container-runner.ts` | .env shadow mount removed, main containers start as root with privilege drop | | `container/Dockerfile` | Entrypoint: `mount --bind` for .env shadowing, `setpriv` privilege drop | | `container/build.sh` | Default runtime: `docker` → `container` | - - -## Diagnostics (Optional) - -After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/customize/SKILL.md b/.claude/skills/customize/SKILL.md index 310f1ed..614a979 100644 --- a/.claude/skills/customize/SKILL.md +++ b/.claude/skills/customize/SKILL.md @@ -108,8 +108,3 @@ User: "Add Telegram as an input channel" 3. Create `src/channels/telegram.ts` implementing the `Channel` interface (see `src/channels/whatsapp.ts`) 4. Add the channel to `main()` in `src/index.ts` 5. Tell user how to authenticate and test - - -## Diagnostics (Optional) - -After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/debug/SKILL.md b/.claude/skills/debug/SKILL.md index e0fc3c7..03c34de 100644 --- a/.claude/skills/debug/SKILL.md +++ b/.claude/skills/debug/SKILL.md @@ -347,8 +347,3 @@ echo -e "\n8. Session continuity working?" SESSIONS=$(grep "Session initialized" logs/nanoclaw.log 2>/dev/null | tail -5 | awk '{print $NF}' | sort -u | wc -l) [ "$SESSIONS" -le 2 ] && echo "OK (recent sessions reusing IDs)" || echo "CHECK - multiple different session IDs, may indicate resumption issues" ``` - - -## Diagnostics (Optional) - -After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/get-qodo-rules/SKILL.md b/.claude/skills/get-qodo-rules/SKILL.md index 4a2cf16..69abaf7 100644 --- a/.claude/skills/get-qodo-rules/SKILL.md +++ b/.claude/skills/get-qodo-rules/SKILL.md @@ -120,8 +120,3 @@ See `~/.qodo/config.json` for API key setup. Set `QODO_ENVIRONMENT_NAME` env var - **Not in git repo** - Inform the user that a git repository is required and exit gracefully; do not attempt code generation - **No API key** - Inform the user with setup instructions; set `QODO_API_KEY` or create `~/.qodo/config.json` - **No rules found** - Inform the user; set up rules at app.qodo.ai - - -## Diagnostics (Optional) - -After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/qodo-pr-resolver/SKILL.md b/.claude/skills/qodo-pr-resolver/SKILL.md index 165bbe2..c0cbe22 100644 --- a/.claude/skills/qodo-pr-resolver/SKILL.md +++ b/.claude/skills/qodo-pr-resolver/SKILL.md @@ -324,8 +324,3 @@ Use the inline comment ID preserved during deduplication (Step 3b) to reply dire See [providers.md § Reply to Inline Comments](./resources/providers.md#reply-to-inline-comments) for provider-specific commands and reply format. Keep replies short (one line). If a reply fails, log it and continue. - - -## Diagnostics (Optional) - -After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/update-skills/SKILL.md b/.claude/skills/update-skills/SKILL.md index ade436b..cbbff39 100644 --- a/.claude/skills/update-skills/SKILL.md +++ b/.claude/skills/update-skills/SKILL.md @@ -128,8 +128,3 @@ Show: - Any conflicts that were resolved (list files) If the service is running, remind the user to restart it to pick up changes. - - -## Diagnostics (Optional) - -After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/use-local-whisper/SKILL.md b/.claude/skills/use-local-whisper/SKILL.md index b7d9814..ec18a09 100644 --- a/.claude/skills/use-local-whisper/SKILL.md +++ b/.claude/skills/use-local-whisper/SKILL.md @@ -150,8 +150,3 @@ whisper-cli -m data/models/ggml-base.bin -f /tmp/test.wav --no-timestamps -nt **Slow transcription**: The base model processes ~30s of audio in <1s on M1+. If slower, check CPU usage — another process may be competing. **Wrong language**: whisper.cpp auto-detects language. To force a language, you can set `WHISPER_LANG` and modify `src/transcription.ts` to pass `-l $WHISPER_LANG`. - - -## Diagnostics (Optional) - -After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. diff --git a/.claude/skills/x-integration/SKILL.md b/.claude/skills/x-integration/SKILL.md index 86ae041..29a7be6 100644 --- a/.claude/skills/x-integration/SKILL.md +++ b/.claude/skills/x-integration/SKILL.md @@ -414,8 +414,4 @@ docker run nanoclaw-agent ls -la /app/src/skills/ - `data/x-browser-profile/` - Contains X session cookies (in `.gitignore`) - `data/x-auth.json` - Auth state marker (in `.gitignore`) - Only main group can use X tools (enforced in `agent.ts` and `host.ts`) -- Scripts run as subprocesses with limited environment - -## Diagnostics (Optional) - -After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. +- Scripts run as subprocesses with limited environment \ No newline at end of file From 31ac74f5f2462fba512d28a69d8d3a1055e5bae8 Mon Sep 17 00:00:00 2001 From: gavrielc Date: Sat, 21 Mar 2026 16:28:36 +0200 Subject: [PATCH 07/14] fix: remove claw skill accidentally added to this branch Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/skills/claw/SKILL.md | 131 ----------------------------------- 1 file changed, 131 deletions(-) delete mode 100644 .claude/skills/claw/SKILL.md diff --git a/.claude/skills/claw/SKILL.md b/.claude/skills/claw/SKILL.md deleted file mode 100644 index 10e0dc3..0000000 --- a/.claude/skills/claw/SKILL.md +++ /dev/null @@ -1,131 +0,0 @@ ---- -name: claw -description: Install the claw CLI tool — run NanoClaw agent containers from the command line without opening a chat app. ---- - -# claw — NanoClaw CLI - -`claw` is a Python CLI that sends prompts directly to a NanoClaw agent container from the terminal. It reads registered groups from the NanoClaw database, picks up secrets from `.env`, and pipes a JSON payload into a container run — no chat app required. - -## What it does - -- Send a prompt to any registered group by name, folder, or JID -- Default target is the main group (no `-g` needed for most use) -- Resume a previous session with `-s ` -- Read prompts from stdin (`--pipe`) for scripting and piping -- List all registered groups with `--list-groups` -- Auto-detects `container` or `docker` runtime (or override with `--runtime`) -- Prints the agent's response to stdout; session ID to stderr -- Verbose mode (`-v`) shows the command, redacted payload, and exit code - -## Prerequisites - -- Python 3.8 or later -- NanoClaw installed with a built and tagged container image (`nanoclaw-agent:latest`) -- Either `container` (Apple Container, macOS 15+) or `docker` available in `PATH` - -## Install - -Run this skill from within the NanoClaw directory. The script auto-detects its location, so the symlink always points to the right place. - -### 1. Copy the script - -```bash -mkdir -p scripts -cp "${CLAUDE_SKILL_DIR}/scripts/claw" scripts/claw -chmod +x scripts/claw -``` - -### 2. Symlink into PATH - -```bash -mkdir -p ~/bin -ln -sf "$(pwd)/scripts/claw" ~/bin/claw -``` - -Make sure `~/bin` is in `PATH`. Add this to `~/.zshrc` or `~/.bashrc` if needed: - -```bash -export PATH="$HOME/bin:$PATH" -``` - -Then reload the shell: - -```bash -source ~/.zshrc # or ~/.bashrc -``` - -### 3. Verify - -```bash -claw --list-groups -``` - -You should see registered groups. If NanoClaw isn't running or the database doesn't exist yet, the list will be empty — that's fine. - -## Usage Examples - -```bash -# Send a prompt to the main group -claw "What's on my calendar today?" - -# Send to a specific group by name (fuzzy match) -claw -g "family" "Remind everyone about dinner at 7" - -# Send to a group by exact JID -claw -j "120363336345536173@g.us" "Hello" - -# Resume a previous session -claw -s abc123 "Continue where we left off" - -# Read prompt from stdin -echo "Summarize this" | claw --pipe -g dev - -# Pipe a file -cat report.txt | claw --pipe "Summarize this report" - -# List all registered groups -claw --list-groups - -# Force a specific runtime -claw --runtime docker "Hello" - -# Use a custom image tag (e.g. after rebuilding with a new tag) -claw --image nanoclaw-agent:dev "Hello" - -# Verbose mode (debug info, secrets redacted) -claw -v "Hello" - -# Custom timeout for long-running tasks -claw --timeout 600 "Run the full analysis" -``` - -## Troubleshooting - -### "neither 'container' nor 'docker' found" - -Install Docker Desktop or Apple Container (macOS 15+), or pass `--runtime` explicitly. - -### "no secrets found in .env" - -The script auto-detects your NanoClaw directory and reads `.env` from it. Check that the file exists and contains at least one of: `CLAUDE_CODE_OAUTH_TOKEN`, `ANTHROPIC_API_KEY`, `ANTHROPIC_AUTH_TOKEN`. - -### Container times out - -The default timeout is 300 seconds. For longer tasks, pass `--timeout 600` (or higher). If the container consistently hangs, check that your `nanoclaw-agent:latest` image is up to date by running `./container/build.sh`. - -### "group not found" - -Run `claw --list-groups` to see what's registered. Group lookup does a fuzzy partial match on name and folder — if your query matches multiple groups, you'll get an error listing the ambiguous matches. - -### Container crashes mid-stream - -Containers run with `--rm` so they are automatically removed. If the agent crashes before emitting the output sentinel, `claw` falls back to printing raw stdout. Use `-v` to see what the container produced. Rebuild the image with `./container/build.sh` if crashes are consistent. - -### Override the NanoClaw directory - -If `claw` can't find your database or `.env`, set the `NANOCLAW_DIR` environment variable: - -```bash -export NANOCLAW_DIR=/path/to/your/nanoclaw -``` From e10b136df6f15cd65d5a020f947289b479676895 Mon Sep 17 00:00:00 2001 From: gavrielc Date: Sat, 21 Mar 2026 16:31:59 +0200 Subject: [PATCH 08/14] refactor: move diagnostics into each skill's own directory Replace shared _shared/diagnostics.md with dedicated diagnostics.md files in setup/ and update-nanoclaw/. Each contains only the event types relevant to that skill. References updated to local links. Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/skills/_shared/diagnostics.md | 106 ------------------ .claude/skills/setup/SKILL.md | 2 +- .claude/skills/setup/diagnostics.md | 81 +++++++++++++ .claude/skills/update-nanoclaw/SKILL.md | 2 +- .claude/skills/update-nanoclaw/diagnostics.md | 59 ++++++++++ 5 files changed, 142 insertions(+), 108 deletions(-) delete mode 100644 .claude/skills/_shared/diagnostics.md create mode 100644 .claude/skills/setup/diagnostics.md create mode 100644 .claude/skills/update-nanoclaw/diagnostics.md diff --git a/.claude/skills/_shared/diagnostics.md b/.claude/skills/_shared/diagnostics.md deleted file mode 100644 index ac5994b..0000000 --- a/.claude/skills/_shared/diagnostics.md +++ /dev/null @@ -1,106 +0,0 @@ -# Diagnostics (end-of-skill instructions) - -Follow these steps **after all other skill steps are complete**. - -## 1. Collect all events - -Every skill sends its own diagnostics event — even when invoked from a parent skill. A `/setup` that invokes `/add-telegram` produces **two** events: `skill_applied` (for add-telegram) and `setup_complete` (for setup). A `/setup` with no channels produces just `setup_complete`. - -Build the list of events to send: - -1. If sub-skills were invoked during this run (e.g., `/add-telegram` during `/setup`), add a `skill_applied` event for each. -2. Add the event for the current (outermost) skill. - -**If running a sub-skill standalone** (not called from a parent), there is just one event. - -## 2. Determine event type (for each event) - -- `/setup` → `setup_complete` -- `/update-nanoclaw` → `update_complete` -- Any other skill → `skill_applied` - -## 3. Build event-specific data - -Estimate `error_count` from the conversation (how many errors/retries occurred during the run). - -**For `setup_complete`:** -```json -{ - "channels_selected": ["whatsapp", "telegram"], - "error_count": 0, - "failed_step": null, - "exit_code": null -} -``` -- `channels_selected`: which channels the user chose to install -- `failed_step`: if setup failed, which step (environment / container / groups / register / mounts / service / verify) -- `exit_code`: if a step failed, its exit code - -**For `skill_applied`:** -```json -{ - "skill_name": "add-telegram", - "is_upstream_skill": true, - "conflict_files": ["package.json", "src/index.ts"], - "error_count": 0 -} -``` -- `skill_name`: the skill that was run. Use the upstream skill name if it's an upstream skill, otherwise use `"custom"` -- `is_upstream_skill`: true if this is a known upstream skill -- `conflict_files`: filenames that had merge conflicts (the script will gate these against upstream — pass them all, the script filters) - -**For `update_complete`:** -```json -{ - "version_age_days": 45, - "update_method": "merge", - "conflict_files": ["package.json"], - "breaking_changes_found": false, - "breaking_changes_skills_run": [], - "error_count": 0 -} -``` -- `version_age_days`: estimate from the backup tag or commit date how many days old the previous version was -- `update_method`: "merge" or "rebase" -- `breaking_changes_found`: whether breaking changes were detected during the update -- `breaking_changes_skills_run`: which skills had to be re-run to fix breaking changes - -## 4. Dry run all events - -For **each** event, run with `--dry-run` to get the payload: - -```bash -npx tsx scripts/send-diagnostics.ts --event --success --data '' --dry-run -``` - -Use `--failure` instead of `--success` if that particular skill/step failed. - -If **any** dry-run produces no output, the user has opted out permanently — skip the rest for all events. - -## 5. Show the user and ask once - -Show **all** payloads together and ask **once** (not per-event): - -> "Would you like to send anonymous diagnostics to help improve NanoClaw? Here's exactly what would be sent:" -> -> (show all JSON payloads) -> -> **Yes** / **No** / **Never ask again** - -Use AskUserQuestion. - -## 6. Handle response - -- **Yes**: Send **all** events (run each command without `--dry-run`): - ```bash - npx tsx scripts/send-diagnostics.ts --event --success --data '' - ``` - Confirm: "Diagnostics sent (N events)." or "Diagnostics sent." if only one. - -- **No**: Do nothing. User will be asked again next time. - -- **Never ask again**: Run: - ```bash - npx tsx -e "import { setNeverAsk } from './scripts/send-diagnostics.ts'; setNeverAsk();" - ``` - Confirm: "Got it — you won't be asked again." diff --git a/.claude/skills/setup/SKILL.md b/.claude/skills/setup/SKILL.md index 170e303..c1c5d60 100644 --- a/.claude/skills/setup/SKILL.md +++ b/.claude/skills/setup/SKILL.md @@ -220,4 +220,4 @@ Tell user to test: send a message in their registered chat. Show: `tail -f logs/ ## Diagnostics (Optional) -After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. +After completing all steps above, read and follow [diagnostics.md](diagnostics.md). diff --git a/.claude/skills/setup/diagnostics.md b/.claude/skills/setup/diagnostics.md new file mode 100644 index 0000000..f371e41 --- /dev/null +++ b/.claude/skills/setup/diagnostics.md @@ -0,0 +1,81 @@ +# Diagnostics (end-of-setup instructions) + +Follow these steps **after all other setup steps are complete**. + +## 1. Collect events + +Build the list of events to send: + +1. If channel skills were invoked during setup (e.g., `/add-telegram`), add a `skill_applied` event for each. +2. Add a `setup_complete` event for setup itself. + +A `/setup` with no channels produces just `setup_complete`. + +## 2. Build event data + +Estimate `error_count` from the conversation (how many errors/retries occurred). + +**For `setup_complete`:** +```json +{ + "channels_selected": ["whatsapp", "telegram"], + "error_count": 0, + "failed_step": null, + "exit_code": null +} +``` +- `channels_selected`: which channels the user chose to install +- `failed_step`: if setup failed, which step (environment / container / groups / register / mounts / service / verify) +- `exit_code`: if a step failed, its exit code + +**For each `skill_applied`:** +```json +{ + "skill_name": "add-telegram", + "is_upstream_skill": true, + "conflict_files": ["package.json", "src/index.ts"], + "error_count": 0 +} +``` +- `skill_name`: upstream skill name, or `"custom"` for non-upstream skills +- `conflict_files`: filenames with merge conflicts (the script gates these against upstream) + +## 3. Dry run all events + +For **each** event, run with `--dry-run` to get the payload: + +```bash +npx tsx scripts/send-diagnostics.ts --event --success --data '' --dry-run +``` + +Use `--failure` instead of `--success` if that particular skill/step failed. + +If **any** dry-run produces no output, the user has opted out permanently — skip the rest. + +## 4. Show the user and ask once + +Show **all** payloads together and ask **once** (not per-event): + +> "Would you like to send anonymous diagnostics to help improve NanoClaw? Here's exactly what would be sent:" +> +> (show all JSON payloads) +> +> **Yes** / **No** / **Never ask again** + +Use AskUserQuestion. + +## 5. Handle response + +- **Yes**: Send **all** events (run each command without `--dry-run`): + ```bash + npx tsx scripts/send-diagnostics.ts --event --success --data '' + ``` + Confirm: "Diagnostics sent (N events)." or "Diagnostics sent." if only one. + +- **No**: Do nothing. User will be asked again next time. + +- **Never ask again**: Run: + ```bash + npx tsx -e "import { setNeverAsk } from './scripts/send-diagnostics.ts'; setNeverAsk();" + ``` + Confirm: "Got it — you won't be asked again." diff --git a/.claude/skills/update-nanoclaw/SKILL.md b/.claude/skills/update-nanoclaw/SKILL.md index 5e93c6c..e8197d2 100644 --- a/.claude/skills/update-nanoclaw/SKILL.md +++ b/.claude/skills/update-nanoclaw/SKILL.md @@ -237,4 +237,4 @@ Tell the user: ## Diagnostics (Optional) -After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`. +After completing all steps above, read and follow [diagnostics.md](diagnostics.md). diff --git a/.claude/skills/update-nanoclaw/diagnostics.md b/.claude/skills/update-nanoclaw/diagnostics.md new file mode 100644 index 0000000..9028dc8 --- /dev/null +++ b/.claude/skills/update-nanoclaw/diagnostics.md @@ -0,0 +1,59 @@ +# Diagnostics (end-of-update instructions) + +Follow these steps **after all other update steps are complete**. + +## 1. Build event data + +Estimate `error_count` from the conversation (how many errors/retries occurred). + +```json +{ + "version_age_days": 45, + "update_method": "merge", + "conflict_files": ["package.json"], + "breaking_changes_found": false, + "breaking_changes_skills_run": [], + "error_count": 0 +} +``` +- `version_age_days`: estimate from the backup tag or commit date how many days old the previous version was +- `update_method`: "merge" or "rebase" +- `conflict_files`: filenames with merge conflicts (the script gates these against upstream) +- `breaking_changes_found`: whether breaking changes were detected +- `breaking_changes_skills_run`: which skills had to be re-run to fix breaking changes + +## 2. Dry run + +```bash +npx tsx scripts/send-diagnostics.ts --event update_complete --success --data '' --dry-run +``` + +Use `--failure` instead of `--success` if the update failed. + +If the dry-run produces no output, the user has opted out permanently — skip the rest. + +## 3. Show the user and ask + +> "Would you like to send anonymous diagnostics to help improve NanoClaw? Here's exactly what would be sent:" +> +> (show JSON payload) +> +> **Yes** / **No** / **Never ask again** + +Use AskUserQuestion. + +## 4. Handle response + +- **Yes**: Run without `--dry-run`: + ```bash + npx tsx scripts/send-diagnostics.ts --event update_complete --success --data '' + ``` + Confirm: "Diagnostics sent." + +- **No**: Do nothing. User will be asked again next time. + +- **Never ask again**: Run: + ```bash + npx tsx -e "import { setNeverAsk } from './scripts/send-diagnostics.ts'; setNeverAsk();" + ``` + Confirm: "Got it — you won't be asked again." From e2423171e1579f1cf6713a68153030647990f8b1 Mon Sep 17 00:00:00 2001 From: gavrielc Date: Sat, 21 Mar 2026 18:36:08 +0200 Subject: [PATCH 09/14] simplify diagnostics instructions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Show example commands with placeholder values. Claude fills in the actual values from the session in one shot — no multi-step build process. Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/skills/setup/diagnostics.md | 87 +++++-------------- .claude/skills/update-nanoclaw/diagnostics.md | 58 ++++--------- 2 files changed, 42 insertions(+), 103 deletions(-) diff --git a/.claude/skills/setup/diagnostics.md b/.claude/skills/setup/diagnostics.md index f371e41..3f463b9 100644 --- a/.claude/skills/setup/diagnostics.md +++ b/.claude/skills/setup/diagnostics.md @@ -1,81 +1,42 @@ -# Diagnostics (end-of-setup instructions) +# Diagnostics (Optional) -Follow these steps **after all other setup steps are complete**. +After setup is complete, offer to send anonymous diagnostics. -## 1. Collect events - -Build the list of events to send: - -1. If channel skills were invoked during setup (e.g., `/add-telegram`), add a `skill_applied` event for each. -2. Add a `setup_complete` event for setup itself. - -A `/setup` with no channels produces just `setup_complete`. - -## 2. Build event data - -Estimate `error_count` from the conversation (how many errors/retries occurred). - -**For `setup_complete`:** -```json -{ - "channels_selected": ["whatsapp", "telegram"], - "error_count": 0, - "failed_step": null, - "exit_code": null -} -``` -- `channels_selected`: which channels the user chose to install -- `failed_step`: if setup failed, which step (environment / container / groups / register / mounts / service / verify) -- `exit_code`: if a step failed, its exit code - -**For each `skill_applied`:** -```json -{ - "skill_name": "add-telegram", - "is_upstream_skill": true, - "conflict_files": ["package.json", "src/index.ts"], - "error_count": 0 -} -``` -- `skill_name`: upstream skill name, or `"custom"` for non-upstream skills -- `conflict_files`: filenames with merge conflicts (the script gates these against upstream) - -## 3. Dry run all events - -For **each** event, run with `--dry-run` to get the payload: +## 1. Check opt-out ```bash -npx tsx scripts/send-diagnostics.ts --event --success --data '' --dry-run +npx tsx scripts/send-diagnostics.ts --event setup_complete --success --data '{}' --dry-run ``` -Use `--failure` instead of `--success` if that particular skill/step failed. +If no output, the user opted out permanently — stop here. -If **any** dry-run produces no output, the user has opted out permanently — skip the rest. +## 2. Prepare events -## 4. Show the user and ask once +For each channel skill invoked during setup (e.g. `/add-telegram`), prepare a `skill_applied` event. Then prepare a `setup_complete` event for setup itself. -Show **all** payloads together and ask **once** (not per-event): +Run `--dry-run` for each to get the final payload: + +```bash +npx tsx scripts/send-diagnostics.ts --event skill_applied --success --data '{"skill_name":"add-telegram","is_upstream_skill":true,"conflict_files":[],"error_count":0}' --dry-run +npx tsx scripts/send-diagnostics.ts --event setup_complete --success --data '{"channels_selected":["telegram"],"error_count":0,"failed_step":null,"exit_code":null}' --dry-run +``` + +Use `--failure` instead of `--success` if that step failed. Fill in the values based on what actually happened during the session. + +## 3. Ask the user + +Show all payloads and ask once: > "Would you like to send anonymous diagnostics to help improve NanoClaw? Here's exactly what would be sent:" > -> (show all JSON payloads) +> (show JSON payloads) > > **Yes** / **No** / **Never ask again** Use AskUserQuestion. -## 5. Handle response +## 4. Handle response -- **Yes**: Send **all** events (run each command without `--dry-run`): - ```bash - npx tsx scripts/send-diagnostics.ts --event --success --data '' - ``` - Confirm: "Diagnostics sent (N events)." or "Diagnostics sent." if only one. - -- **No**: Do nothing. User will be asked again next time. - -- **Never ask again**: Run: - ```bash - npx tsx -e "import { setNeverAsk } from './scripts/send-diagnostics.ts'; setNeverAsk();" - ``` - Confirm: "Got it — you won't be asked again." +- **Yes**: Run each command again without `--dry-run`. Confirm: "Diagnostics sent." +- **No**: Do nothing. +- **Never ask again**: Run `npx tsx -e "import { setNeverAsk } from './scripts/send-diagnostics.ts'; setNeverAsk();"` — confirm: "Got it — you won't be asked again." diff --git a/.claude/skills/update-nanoclaw/diagnostics.md b/.claude/skills/update-nanoclaw/diagnostics.md index 9028dc8..58638b3 100644 --- a/.claude/skills/update-nanoclaw/diagnostics.md +++ b/.claude/skills/update-nanoclaw/diagnostics.md @@ -1,38 +1,26 @@ -# Diagnostics (end-of-update instructions) +# Diagnostics (Optional) -Follow these steps **after all other update steps are complete**. +After the update is complete, offer to send anonymous diagnostics. -## 1. Build event data - -Estimate `error_count` from the conversation (how many errors/retries occurred). - -```json -{ - "version_age_days": 45, - "update_method": "merge", - "conflict_files": ["package.json"], - "breaking_changes_found": false, - "breaking_changes_skills_run": [], - "error_count": 0 -} -``` -- `version_age_days`: estimate from the backup tag or commit date how many days old the previous version was -- `update_method`: "merge" or "rebase" -- `conflict_files`: filenames with merge conflicts (the script gates these against upstream) -- `breaking_changes_found`: whether breaking changes were detected -- `breaking_changes_skills_run`: which skills had to be re-run to fix breaking changes - -## 2. Dry run +## 1. Check opt-out ```bash -npx tsx scripts/send-diagnostics.ts --event update_complete --success --data '' --dry-run +npx tsx scripts/send-diagnostics.ts --event update_complete --success --data '{}' --dry-run ``` -Use `--failure` instead of `--success` if the update failed. +If no output, the user opted out permanently — stop here. -If the dry-run produces no output, the user has opted out permanently — skip the rest. +## 2. Prepare event -## 3. Show the user and ask +Run `--dry-run` to get the final payload: + +```bash +npx tsx scripts/send-diagnostics.ts --event update_complete --success --data '{"version_age_days":45,"update_method":"merge","conflict_files":[],"breaking_changes_found":false,"breaking_changes_skills_run":[],"error_count":0}' --dry-run +``` + +Use `--failure` instead of `--success` if the update failed. Fill in the values based on what actually happened during the session. + +## 3. Ask the user > "Would you like to send anonymous diagnostics to help improve NanoClaw? Here's exactly what would be sent:" > @@ -44,16 +32,6 @@ Use AskUserQuestion. ## 4. Handle response -- **Yes**: Run without `--dry-run`: - ```bash - npx tsx scripts/send-diagnostics.ts --event update_complete --success --data '' - ``` - Confirm: "Diagnostics sent." - -- **No**: Do nothing. User will be asked again next time. - -- **Never ask again**: Run: - ```bash - npx tsx -e "import { setNeverAsk } from './scripts/send-diagnostics.ts'; setNeverAsk();" - ``` - Confirm: "Got it — you won't be asked again." +- **Yes**: Run the command again without `--dry-run`. Confirm: "Diagnostics sent." +- **No**: Do nothing. +- **Never ask again**: Run `npx tsx -e "import { setNeverAsk } from './scripts/send-diagnostics.ts'; setNeverAsk();"` — confirm: "Got it — you won't be asked again." From f33c66b04650b120b87f9e096bd33b52854d43f7 Mon Sep 17 00:00:00 2001 From: gavrielc Date: Sat, 21 Mar 2026 18:37:13 +0200 Subject: [PATCH 10/14] simplify setup diagnostics to single event One setup_complete event at the end, not per-skill events. Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/skills/setup/diagnostics.md | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/.claude/skills/setup/diagnostics.md b/.claude/skills/setup/diagnostics.md index 3f463b9..d59f633 100644 --- a/.claude/skills/setup/diagnostics.md +++ b/.claude/skills/setup/diagnostics.md @@ -10,26 +10,21 @@ npx tsx scripts/send-diagnostics.ts --event setup_complete --success --data '{}' If no output, the user opted out permanently — stop here. -## 2. Prepare events - -For each channel skill invoked during setup (e.g. `/add-telegram`), prepare a `skill_applied` event. Then prepare a `setup_complete` event for setup itself. - -Run `--dry-run` for each to get the final payload: +## 2. Dry run ```bash -npx tsx scripts/send-diagnostics.ts --event skill_applied --success --data '{"skill_name":"add-telegram","is_upstream_skill":true,"conflict_files":[],"error_count":0}' --dry-run npx tsx scripts/send-diagnostics.ts --event setup_complete --success --data '{"channels_selected":["telegram"],"error_count":0,"failed_step":null,"exit_code":null}' --dry-run ``` -Use `--failure` instead of `--success` if that step failed. Fill in the values based on what actually happened during the session. +Use `--failure` instead of `--success` if setup failed. Fill in the values based on what happened during the session. ## 3. Ask the user -Show all payloads and ask once: +Show the payload and ask: > "Would you like to send anonymous diagnostics to help improve NanoClaw? Here's exactly what would be sent:" > -> (show JSON payloads) +> (show JSON payload) > > **Yes** / **No** / **Never ask again** @@ -37,6 +32,6 @@ Use AskUserQuestion. ## 4. Handle response -- **Yes**: Run each command again without `--dry-run`. Confirm: "Diagnostics sent." +- **Yes**: Run the command again without `--dry-run`. Confirm: "Diagnostics sent." - **No**: Do nothing. - **Never ask again**: Run `npx tsx -e "import { setNeverAsk } from './scripts/send-diagnostics.ts'; setNeverAsk();"` — confirm: "Got it — you won't be asked again." From 09d833c310a34560979ac4c277a5ef601b8cd20b Mon Sep 17 00:00:00 2001 From: gavrielc Date: Sat, 21 Mar 2026 18:45:04 +0200 Subject: [PATCH 11/14] replace diagnostics script with curl, simplify flow Remove send-diagnostics.ts entirely. Claude writes the JSON, shows it to the user, and sends via curl. Opt-out is permanent: Claude replaces diagnostics.md contents and removes the section from SKILL.md. No dependencies, no state files, no .nanoclaw/ directory. Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/skills/setup/diagnostics.md | 58 ++-- .claude/skills/update-nanoclaw/diagnostics.md | 60 ++-- scripts/send-diagnostics.ts | 315 ------------------ 3 files changed, 80 insertions(+), 353 deletions(-) delete mode 100644 scripts/send-diagnostics.ts diff --git a/.claude/skills/setup/diagnostics.md b/.claude/skills/setup/diagnostics.md index d59f633..deb6509 100644 --- a/.claude/skills/setup/diagnostics.md +++ b/.claude/skills/setup/diagnostics.md @@ -2,36 +2,56 @@ After setup is complete, offer to send anonymous diagnostics. -## 1. Check opt-out +## 1. Write the event -```bash -npx tsx scripts/send-diagnostics.ts --event setup_complete --success --data '{}' --dry-run +Write a JSON file to `/tmp/nanoclaw-diagnostics.json` with the setup outcome. Use only non-identifying information — no paths, usernames, hostnames, or IP addresses. + +```json +{ + "event": "setup_complete", + "success": true, + "properties": { + "nanoclaw_version": "1.2.21", + "os_platform": "darwin", + "arch": "arm64", + "node_major_version": 22, + "channels_selected": ["telegram", "whatsapp"], + "error_count": 0, + "failed_step": null + } +} ``` -If no output, the user opted out permanently — stop here. +Fill in the values based on what happened during the session. -## 2. Dry run +## 2. Show and ask -```bash -npx tsx scripts/send-diagnostics.ts --event setup_complete --success --data '{"channels_selected":["telegram"],"error_count":0,"failed_step":null,"exit_code":null}' --dry-run -``` - -Use `--failure` instead of `--success` if setup failed. Fill in the values based on what happened during the session. - -## 3. Ask the user - -Show the payload and ask: +Show the contents of the file to the user and ask: > "Would you like to send anonymous diagnostics to help improve NanoClaw? Here's exactly what would be sent:" > -> (show JSON payload) +> (show JSON) > > **Yes** / **No** / **Never ask again** Use AskUserQuestion. -## 4. Handle response +## 3. Handle response -- **Yes**: Run the command again without `--dry-run`. Confirm: "Diagnostics sent." -- **No**: Do nothing. -- **Never ask again**: Run `npx tsx -e "import { setNeverAsk } from './scripts/send-diagnostics.ts'; setNeverAsk();"` — confirm: "Got it — you won't be asked again." +**Yes**: Send it: +```bash +curl -s -X POST https://us.i.posthog.com/capture/ \ + -H 'Content-Type: application/json' \ + -d "{\"api_key\":\"phc_fx1Hhx9ucz8GuaJC8LVZWO8u03yXZZJJ6ObS4yplnaP\",\"event\":\"$(jq -r .event /tmp/nanoclaw-diagnostics.json)\",\"distinct_id\":\"$(uuidgen)\",\"properties\":$(jq .properties /tmp/nanoclaw-diagnostics.json)}" +rm /tmp/nanoclaw-diagnostics.json +``` +Confirm: "Diagnostics sent." + +**No**: `rm /tmp/nanoclaw-diagnostics.json` — do nothing else. + +**Never ask again**: Replace the contents of this file (`diagnostics.md`) with: +``` +# Diagnostics — opted out +``` +Then remove the "Diagnostics (Optional)" section from the end of `SKILL.md` in this skill directory. Delete `/tmp/nanoclaw-diagnostics.json`. +Confirm: "Got it — you won't be asked again." diff --git a/.claude/skills/update-nanoclaw/diagnostics.md b/.claude/skills/update-nanoclaw/diagnostics.md index 58638b3..5d98855 100644 --- a/.claude/skills/update-nanoclaw/diagnostics.md +++ b/.claude/skills/update-nanoclaw/diagnostics.md @@ -2,36 +2,58 @@ After the update is complete, offer to send anonymous diagnostics. -## 1. Check opt-out +## 1. Write the event -```bash -npx tsx scripts/send-diagnostics.ts --event update_complete --success --data '{}' --dry-run +Write a JSON file to `/tmp/nanoclaw-diagnostics.json` with the update outcome. Use only non-identifying information — no paths, usernames, hostnames, or IP addresses. + +```json +{ + "event": "update_complete", + "success": true, + "properties": { + "nanoclaw_version": "1.2.21", + "os_platform": "darwin", + "arch": "arm64", + "node_major_version": 22, + "version_age_days": 45, + "update_method": "merge", + "conflict_count": 0, + "breaking_changes_found": false, + "error_count": 0 + } +} ``` -If no output, the user opted out permanently — stop here. +Fill in the values based on what happened during the session. -## 2. Prepare event +## 2. Show and ask -Run `--dry-run` to get the final payload: - -```bash -npx tsx scripts/send-diagnostics.ts --event update_complete --success --data '{"version_age_days":45,"update_method":"merge","conflict_files":[],"breaking_changes_found":false,"breaking_changes_skills_run":[],"error_count":0}' --dry-run -``` - -Use `--failure` instead of `--success` if the update failed. Fill in the values based on what actually happened during the session. - -## 3. Ask the user +Show the contents of the file to the user and ask: > "Would you like to send anonymous diagnostics to help improve NanoClaw? Here's exactly what would be sent:" > -> (show JSON payload) +> (show JSON) > > **Yes** / **No** / **Never ask again** Use AskUserQuestion. -## 4. Handle response +## 3. Handle response -- **Yes**: Run the command again without `--dry-run`. Confirm: "Diagnostics sent." -- **No**: Do nothing. -- **Never ask again**: Run `npx tsx -e "import { setNeverAsk } from './scripts/send-diagnostics.ts'; setNeverAsk();"` — confirm: "Got it — you won't be asked again." +**Yes**: Send it: +```bash +curl -s -X POST https://us.i.posthog.com/capture/ \ + -H 'Content-Type: application/json' \ + -d "{\"api_key\":\"phc_fx1Hhx9ucz8GuaJC8LVZWO8u03yXZZJJ6ObS4yplnaP\",\"event\":\"$(jq -r .event /tmp/nanoclaw-diagnostics.json)\",\"distinct_id\":\"$(uuidgen)\",\"properties\":$(jq .properties /tmp/nanoclaw-diagnostics.json)}" +rm /tmp/nanoclaw-diagnostics.json +``` +Confirm: "Diagnostics sent." + +**No**: `rm /tmp/nanoclaw-diagnostics.json` — do nothing else. + +**Never ask again**: Replace the contents of this file (`diagnostics.md`) with: +``` +# Diagnostics — opted out +``` +Then remove the "Diagnostics (Optional)" section from the end of `SKILL.md` in this skill directory. Delete `/tmp/nanoclaw-diagnostics.json`. +Confirm: "Got it — you won't be asked again." diff --git a/scripts/send-diagnostics.ts b/scripts/send-diagnostics.ts deleted file mode 100644 index 5b5399c..0000000 --- a/scripts/send-diagnostics.ts +++ /dev/null @@ -1,315 +0,0 @@ -/** - * send-diagnostics.ts — opt-in, privacy-first diagnostics for NanoClaw. - * - * Collects system info, accepts event-specific data via --data JSON arg, - * gates conflict filenames against upstream, and sends to PostHog. - * - * Usage: - * npx tsx scripts/send-diagnostics.ts \ - * --event \ - * [--success|--failure] \ - * [--data ''] \ - * [--dry-run] - * - * Never exits non-zero on telemetry failures. - */ - -import { execSync } from 'child_process'; -import crypto from 'crypto'; -import fs from 'fs'; -import path from 'path'; -import { parse as parseYaml, stringify as stringifyYaml } from 'yaml'; - -const POSTHOG_ENDPOINT = 'https://us.i.posthog.com/capture/'; -const POSTHOG_TOKEN = 'phc_fx1Hhx9ucz8GuaJC8LVZWO8u03yXZZJJ6ObS4yplnaP'; -const SEND_TIMEOUT_MS = 5000; - -const PROJECT_ROOT = path.resolve(import.meta.dirname, '..'); -const STATE_YAML_PATH = path.join(PROJECT_ROOT, '.nanoclaw', 'state.yaml'); - -// --- Args --- - -function parseArgs(): { - event: string; - success?: boolean; - data: Record; - dryRun: boolean; -} { - const args = process.argv.slice(2); - let event = ''; - let success: boolean | undefined; - let data: Record = {}; - let dryRun = false; - - for (let i = 0; i < args.length; i++) { - switch (args[i]) { - case '--event': - event = args[++i] || ''; - break; - case '--success': - success = true; - break; - case '--failure': - success = false; - break; - case '--data': - try { - data = JSON.parse(args[++i] || '{}'); - } catch { - console.error('Warning: --data JSON parse failed, ignoring'); - } - break; - case '--dry-run': - dryRun = true; - break; - } - } - - if (!event) { - console.error('Error: --event is required'); - process.exit(0); // exit 0 — never fail on diagnostics - } - - return { event, success, data, dryRun }; -} - -// --- State (neverAsk) --- - -function readState(): Record { - try { - const raw = fs.readFileSync(STATE_YAML_PATH, 'utf-8'); - return parseYaml(raw) || {}; - } catch { - return {}; - } -} - -function isNeverAsk(): boolean { - const state = readState(); - return state.neverAsk === true; -} - -export function setNeverAsk(): void { - const state = readState(); - state.neverAsk = true; - const dir = path.dirname(STATE_YAML_PATH); - fs.mkdirSync(dir, { recursive: true }); - fs.writeFileSync(STATE_YAML_PATH, stringifyYaml(state)); -} - -// --- Git helpers --- - -/** Resolve the upstream remote ref (could be 'upstream/main' or 'origin/main'). */ -function resolveUpstreamRef(): string | null { - for (const ref of ['upstream/main', 'origin/main']) { - try { - execSync(`git rev-parse --verify ${ref}`, { - cwd: PROJECT_ROOT, - stdio: 'ignore', - }); - return ref; - } catch { - continue; - } - } - return null; -} - -// --- System info --- - -function getNanoclawVersion(): string { - try { - const pkg = JSON.parse( - fs.readFileSync(path.join(PROJECT_ROOT, 'package.json'), 'utf-8'), - ); - return pkg.version || 'unknown'; - } catch { - return 'unknown'; - } -} - -function getNodeMajorVersion(): number | null { - const match = process.version.match(/^v(\d+)/); - return match ? parseInt(match[1], 10) : null; -} - -function getContainerRuntime(): string { - try { - const src = fs.readFileSync( - path.join(PROJECT_ROOT, 'src', 'container-runtime.ts'), - 'utf-8', - ); - const match = src.match(/CONTAINER_RUNTIME_BIN\s*=\s*['"]([^'"]+)['"]/); - return match ? match[1] : 'unknown'; - } catch { - return 'unknown'; - } -} - -function isUpstreamCommit(): boolean { - const ref = resolveUpstreamRef(); - if (!ref) return false; - try { - const head = execSync('git rev-parse HEAD', { - encoding: 'utf-8', - cwd: PROJECT_ROOT, - stdio: ['pipe', 'pipe', 'ignore'], - }).trim(); - execSync(`git merge-base --is-ancestor ${head} ${ref}`, { - cwd: PROJECT_ROOT, - stdio: 'ignore', - }); - return true; - } catch { - return false; - } -} - -function collectSystemInfo(): Record { - return { - nanoclaw_version: getNanoclawVersion(), - os_platform: process.platform, - arch: process.arch, - node_major_version: getNodeMajorVersion(), - container_runtime: getContainerRuntime(), - is_upstream_commit: isUpstreamCommit(), - }; -} - -// --- Conflict filename gating --- - -function getUpstreamFiles(): Set | null { - const ref = resolveUpstreamRef(); - if (!ref) return null; - try { - const output = execSync(`git ls-tree -r --name-only ${ref}`, { - encoding: 'utf-8', - cwd: PROJECT_ROOT, - stdio: ['pipe', 'pipe', 'ignore'], - }); - return new Set(output.trim().split('\n').filter(Boolean)); - } catch { - return null; - } -} - -function gateConflictFiles(data: Record): void { - if (!Array.isArray(data.conflict_files)) return; - - const rawFiles: string[] = data.conflict_files; - const upstreamFiles = getUpstreamFiles(); - const totalCount = rawFiles.length; - - if (!upstreamFiles) { - // Can't verify — fail-closed - data.conflict_files = []; - data.conflict_count = totalCount; - data.has_non_upstream_conflicts = totalCount > 0; - return; - } - - const safe: string[] = []; - let hasNonUpstream = false; - - for (const file of rawFiles) { - if (upstreamFiles.has(file)) { - safe.push(file); - } else { - hasNonUpstream = true; - } - } - - data.conflict_files = safe; - data.conflict_count = totalCount; - data.has_non_upstream_conflicts = hasNonUpstream; -} - -// --- Build & send --- - -function buildPayload( - event: string, - systemInfo: Record, - eventData: Record, - success?: boolean, -): Record { - const properties: Record = { - $process_person_profile: false, - $lib: 'nanoclaw-diagnostics', - ...systemInfo, - ...eventData, - }; - - if (success !== undefined) { - properties.success = success; - } - - return { - api_key: POSTHOG_TOKEN, - event, - distinct_id: crypto.randomUUID(), - properties, - }; -} - -async function sendToPostHog( - payload: Record, -): Promise { - const controller = new AbortController(); - const timeout = setTimeout(() => controller.abort(), SEND_TIMEOUT_MS); - - try { - const response = await fetch(POSTHOG_ENDPOINT, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(payload), - signal: controller.signal, - }); - if (response.ok) { - console.log('Diagnostics sent successfully.'); - } else { - console.error( - `Diagnostics send failed (HTTP ${response.status}). This is fine.`, - ); - } - } catch (err) { - console.error('Diagnostics send failed (network error). This is fine.'); - } finally { - clearTimeout(timeout); - } -} - -// --- Main --- - -async function main(): Promise { - try { - if (isNeverAsk()) { - // User opted out permanently — exit silently - return; - } - - const { event, success, data, dryRun } = parseArgs(); - - // Gate conflict filenames before building payload - gateConflictFiles(data); - - const systemInfo = collectSystemInfo(); - const payload = buildPayload(event, systemInfo, data, success); - - if (dryRun) { - // Strip internal fields before showing to user - const { api_key, distinct_id, ...visible } = payload; - const props = visible.properties as Record; - delete props.$process_person_profile; - delete props.$lib; - console.log(JSON.stringify(visible, null, 2)); - return; - } - - await sendToPostHog(payload); - } catch (err) { - // Never fail on diagnostics - console.error('Diagnostics error (this is fine):', (err as Error).message); - } -} - -main(); From f97394656c7c358edd495b1ea133a7eb00c3e2e4 Mon Sep 17 00:00:00 2001 From: gavrielc Date: Sat, 21 Mar 2026 18:47:54 +0200 Subject: [PATCH 12/14] cross-skill opt-out and gather system info via shell - "Never ask again" now removes diagnostics from both skills - Added shell commands to gather version, platform, arch, node version - Show only properties object to user, not api_key/distinct_id - Write full PostHog payload to temp file, send with curl -d @file Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/skills/setup/diagnostics.md | 42 ++++++++++------- .claude/skills/update-nanoclaw/diagnostics.md | 45 ++++++++++++------- 2 files changed, 53 insertions(+), 34 deletions(-) diff --git a/.claude/skills/setup/diagnostics.md b/.claude/skills/setup/diagnostics.md index deb6509..2511d06 100644 --- a/.claude/skills/setup/diagnostics.md +++ b/.claude/skills/setup/diagnostics.md @@ -2,15 +2,26 @@ After setup is complete, offer to send anonymous diagnostics. -## 1. Write the event +## 1. Gather system info -Write a JSON file to `/tmp/nanoclaw-diagnostics.json` with the setup outcome. Use only non-identifying information — no paths, usernames, hostnames, or IP addresses. +```bash +node -p "require('./package.json').version" +uname -s +uname -m +node -p "process.versions.node.split('.')[0]" +``` + +## 2. Write the event + +Write a JSON file to `/tmp/nanoclaw-diagnostics.json`. Fill in system info from the commands above and the outcome from the session. Use only non-identifying information — no paths, usernames, hostnames, or IP addresses. ```json { + "api_key": "phc_fx1Hhx9ucz8GuaJC8LVZWO8u03yXZZJJ6ObS4yplnaP", "event": "setup_complete", - "success": true, + "distinct_id": "", "properties": { + "success": true, "nanoclaw_version": "1.2.21", "os_platform": "darwin", "arch": "arm64", @@ -22,36 +33,33 @@ Write a JSON file to `/tmp/nanoclaw-diagnostics.json` with the setup outcome. Us } ``` -Fill in the values based on what happened during the session. - -## 2. Show and ask +## 3. Show and ask Show the contents of the file to the user and ask: > "Would you like to send anonymous diagnostics to help improve NanoClaw? Here's exactly what would be sent:" > -> (show JSON) +> (show the properties object only — not the api_key or distinct_id) > > **Yes** / **No** / **Never ask again** Use AskUserQuestion. -## 3. Handle response +## 4. Handle response -**Yes**: Send it: +**Yes**: ```bash -curl -s -X POST https://us.i.posthog.com/capture/ \ - -H 'Content-Type: application/json' \ - -d "{\"api_key\":\"phc_fx1Hhx9ucz8GuaJC8LVZWO8u03yXZZJJ6ObS4yplnaP\",\"event\":\"$(jq -r .event /tmp/nanoclaw-diagnostics.json)\",\"distinct_id\":\"$(uuidgen)\",\"properties\":$(jq .properties /tmp/nanoclaw-diagnostics.json)}" +curl -s -X POST https://us.i.posthog.com/capture/ -H 'Content-Type: application/json' -d @/tmp/nanoclaw-diagnostics.json rm /tmp/nanoclaw-diagnostics.json ``` Confirm: "Diagnostics sent." **No**: `rm /tmp/nanoclaw-diagnostics.json` — do nothing else. -**Never ask again**: Replace the contents of this file (`diagnostics.md`) with: -``` -# Diagnostics — opted out -``` -Then remove the "Diagnostics (Optional)" section from the end of `SKILL.md` in this skill directory. Delete `/tmp/nanoclaw-diagnostics.json`. +**Never ask again**: Opt out of diagnostics in both setup and update-nanoclaw: +1. Replace the contents of `.claude/skills/setup/diagnostics.md` with just `# Diagnostics — opted out` +2. Replace the contents of `.claude/skills/update-nanoclaw/diagnostics.md` with just `# Diagnostics — opted out` +3. Remove the "Diagnostics (Optional)" section from the end of both `.claude/skills/setup/SKILL.md` and `.claude/skills/update-nanoclaw/SKILL.md` +4. `rm /tmp/nanoclaw-diagnostics.json` + Confirm: "Got it — you won't be asked again." diff --git a/.claude/skills/update-nanoclaw/diagnostics.md b/.claude/skills/update-nanoclaw/diagnostics.md index 5d98855..1ab9bad 100644 --- a/.claude/skills/update-nanoclaw/diagnostics.md +++ b/.claude/skills/update-nanoclaw/diagnostics.md @@ -2,15 +2,29 @@ After the update is complete, offer to send anonymous diagnostics. -## 1. Write the event +## 1. Gather system info -Write a JSON file to `/tmp/nanoclaw-diagnostics.json` with the update outcome. Use only non-identifying information — no paths, usernames, hostnames, or IP addresses. +```bash +node -p "require('./package.json').version" +uname -s +uname -m +node -p "process.versions.node.split('.')[0]" +git log -1 --format=%ci HEAD@{1} 2>/dev/null || echo "unknown" +``` + +The last command gets the date of the previous HEAD (before the update) to estimate version age. + +## 2. Write the event + +Write a JSON file to `/tmp/nanoclaw-diagnostics.json`. Fill in system info from the commands above and the outcome from the session. Use only non-identifying information — no paths, usernames, hostnames, or IP addresses. ```json { + "api_key": "phc_fx1Hhx9ucz8GuaJC8LVZWO8u03yXZZJJ6ObS4yplnaP", "event": "update_complete", - "success": true, + "distinct_id": "", "properties": { + "success": true, "nanoclaw_version": "1.2.21", "os_platform": "darwin", "arch": "arm64", @@ -24,36 +38,33 @@ Write a JSON file to `/tmp/nanoclaw-diagnostics.json` with the update outcome. U } ``` -Fill in the values based on what happened during the session. - -## 2. Show and ask +## 3. Show and ask Show the contents of the file to the user and ask: > "Would you like to send anonymous diagnostics to help improve NanoClaw? Here's exactly what would be sent:" > -> (show JSON) +> (show the properties object only — not the api_key or distinct_id) > > **Yes** / **No** / **Never ask again** Use AskUserQuestion. -## 3. Handle response +## 4. Handle response -**Yes**: Send it: +**Yes**: ```bash -curl -s -X POST https://us.i.posthog.com/capture/ \ - -H 'Content-Type: application/json' \ - -d "{\"api_key\":\"phc_fx1Hhx9ucz8GuaJC8LVZWO8u03yXZZJJ6ObS4yplnaP\",\"event\":\"$(jq -r .event /tmp/nanoclaw-diagnostics.json)\",\"distinct_id\":\"$(uuidgen)\",\"properties\":$(jq .properties /tmp/nanoclaw-diagnostics.json)}" +curl -s -X POST https://us.i.posthog.com/capture/ -H 'Content-Type: application/json' -d @/tmp/nanoclaw-diagnostics.json rm /tmp/nanoclaw-diagnostics.json ``` Confirm: "Diagnostics sent." **No**: `rm /tmp/nanoclaw-diagnostics.json` — do nothing else. -**Never ask again**: Replace the contents of this file (`diagnostics.md`) with: -``` -# Diagnostics — opted out -``` -Then remove the "Diagnostics (Optional)" section from the end of `SKILL.md` in this skill directory. Delete `/tmp/nanoclaw-diagnostics.json`. +**Never ask again**: Opt out of diagnostics in both setup and update-nanoclaw: +1. Replace the contents of `.claude/skills/setup/diagnostics.md` with just `# Diagnostics — opted out` +2. Replace the contents of `.claude/skills/update-nanoclaw/diagnostics.md` with just `# Diagnostics — opted out` +3. Remove the "Diagnostics (Optional)" section from the end of both `.claude/skills/setup/SKILL.md` and `.claude/skills/update-nanoclaw/SKILL.md` +4. `rm /tmp/nanoclaw-diagnostics.json` + Confirm: "Got it — you won't be asked again." From a4fbc9d615a23a4b612441053413835083b875e7 Mon Sep 17 00:00:00 2001 From: gavrielc Date: Sat, 21 Mar 2026 18:51:15 +0200 Subject: [PATCH 13/14] show full payload to user, not just properties Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/skills/setup/diagnostics.md | 2 +- .claude/skills/update-nanoclaw/diagnostics.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.claude/skills/setup/diagnostics.md b/.claude/skills/setup/diagnostics.md index 2511d06..4741777 100644 --- a/.claude/skills/setup/diagnostics.md +++ b/.claude/skills/setup/diagnostics.md @@ -39,7 +39,7 @@ Show the contents of the file to the user and ask: > "Would you like to send anonymous diagnostics to help improve NanoClaw? Here's exactly what would be sent:" > -> (show the properties object only — not the api_key or distinct_id) +> (show the entire JSON file) > > **Yes** / **No** / **Never ask again** diff --git a/.claude/skills/update-nanoclaw/diagnostics.md b/.claude/skills/update-nanoclaw/diagnostics.md index 1ab9bad..46286b7 100644 --- a/.claude/skills/update-nanoclaw/diagnostics.md +++ b/.claude/skills/update-nanoclaw/diagnostics.md @@ -44,7 +44,7 @@ Show the contents of the file to the user and ask: > "Would you like to send anonymous diagnostics to help improve NanoClaw? Here's exactly what would be sent:" > -> (show the properties object only — not the api_key or distinct_id) +> (show the entire JSON file) > > **Yes** / **No** / **Never ask again** From 02d51afe09340db99e38102df05da7850d82c0c8 Mon Sep 17 00:00:00 2001 From: gavrielc Date: Sat, 21 Mar 2026 18:53:53 +0200 Subject: [PATCH 14/14] trim diagnostics verbosity Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/skills/setup/SKILL.md | 4 +- .claude/skills/setup/diagnostics.md | 39 +++++------------- .claude/skills/update-nanoclaw/SKILL.md | 4 +- .claude/skills/update-nanoclaw/diagnostics.md | 41 +++++-------------- 4 files changed, 24 insertions(+), 64 deletions(-) diff --git a/.claude/skills/setup/SKILL.md b/.claude/skills/setup/SKILL.md index c1c5d60..2041f80 100644 --- a/.claude/skills/setup/SKILL.md +++ b/.claude/skills/setup/SKILL.md @@ -218,6 +218,6 @@ Tell user to test: send a message in their registered chat. Show: `tail -f logs/ **Unload service:** macOS: `launchctl unload ~/Library/LaunchAgents/com.nanoclaw.plist` | Linux: `systemctl --user stop nanoclaw` -## Diagnostics (Optional) +## Diagnostics -After completing all steps above, read and follow [diagnostics.md](diagnostics.md). +Read and follow [diagnostics.md](diagnostics.md). diff --git a/.claude/skills/setup/diagnostics.md b/.claude/skills/setup/diagnostics.md index 4741777..a76b4ab 100644 --- a/.claude/skills/setup/diagnostics.md +++ b/.claude/skills/setup/diagnostics.md @@ -1,8 +1,6 @@ -# Diagnostics (Optional) +# Diagnostics -After setup is complete, offer to send anonymous diagnostics. - -## 1. Gather system info +Gather system info: ```bash node -p "require('./package.json').version" @@ -11,15 +9,13 @@ uname -m node -p "process.versions.node.split('.')[0]" ``` -## 2. Write the event - -Write a JSON file to `/tmp/nanoclaw-diagnostics.json`. Fill in system info from the commands above and the outcome from the session. Use only non-identifying information — no paths, usernames, hostnames, or IP addresses. +Write `/tmp/nanoclaw-diagnostics.json`. No paths, usernames, hostnames, or IP addresses. ```json { "api_key": "phc_fx1Hhx9ucz8GuaJC8LVZWO8u03yXZZJJ6ObS4yplnaP", "event": "setup_complete", - "distinct_id": "", + "distinct_id": "", "properties": { "success": true, "nanoclaw_version": "1.2.21", @@ -33,33 +29,18 @@ Write a JSON file to `/tmp/nanoclaw-diagnostics.json`. Fill in system info from } ``` -## 3. Show and ask - -Show the contents of the file to the user and ask: - -> "Would you like to send anonymous diagnostics to help improve NanoClaw? Here's exactly what would be sent:" -> -> (show the entire JSON file) -> -> **Yes** / **No** / **Never ask again** - -Use AskUserQuestion. - -## 4. Handle response +Show the entire JSON to the user and ask via AskUserQuestion: **Yes** / **No** / **Never ask again** **Yes**: ```bash curl -s -X POST https://us.i.posthog.com/capture/ -H 'Content-Type: application/json' -d @/tmp/nanoclaw-diagnostics.json rm /tmp/nanoclaw-diagnostics.json ``` -Confirm: "Diagnostics sent." -**No**: `rm /tmp/nanoclaw-diagnostics.json` — do nothing else. +**No**: `rm /tmp/nanoclaw-diagnostics.json` -**Never ask again**: Opt out of diagnostics in both setup and update-nanoclaw: -1. Replace the contents of `.claude/skills/setup/diagnostics.md` with just `# Diagnostics — opted out` -2. Replace the contents of `.claude/skills/update-nanoclaw/diagnostics.md` with just `# Diagnostics — opted out` -3. Remove the "Diagnostics (Optional)" section from the end of both `.claude/skills/setup/SKILL.md` and `.claude/skills/update-nanoclaw/SKILL.md` +**Never ask again**: +1. Replace contents of `.claude/skills/setup/diagnostics.md` with `# Diagnostics — opted out` +2. Replace contents of `.claude/skills/update-nanoclaw/diagnostics.md` with `# Diagnostics — opted out` +3. Remove the "Diagnostics" section from both `.claude/skills/setup/SKILL.md` and `.claude/skills/update-nanoclaw/SKILL.md` 4. `rm /tmp/nanoclaw-diagnostics.json` - -Confirm: "Got it — you won't be asked again." diff --git a/.claude/skills/update-nanoclaw/SKILL.md b/.claude/skills/update-nanoclaw/SKILL.md index e8197d2..0af5713 100644 --- a/.claude/skills/update-nanoclaw/SKILL.md +++ b/.claude/skills/update-nanoclaw/SKILL.md @@ -235,6 +235,6 @@ Tell the user: - If running manually: restart `npm run dev` -## Diagnostics (Optional) +## Diagnostics -After completing all steps above, read and follow [diagnostics.md](diagnostics.md). +Read and follow [diagnostics.md](diagnostics.md). diff --git a/.claude/skills/update-nanoclaw/diagnostics.md b/.claude/skills/update-nanoclaw/diagnostics.md index 46286b7..bc659ab 100644 --- a/.claude/skills/update-nanoclaw/diagnostics.md +++ b/.claude/skills/update-nanoclaw/diagnostics.md @@ -1,8 +1,6 @@ -# Diagnostics (Optional) +# Diagnostics -After the update is complete, offer to send anonymous diagnostics. - -## 1. Gather system info +Gather system info: ```bash node -p "require('./package.json').version" @@ -12,17 +10,13 @@ node -p "process.versions.node.split('.')[0]" git log -1 --format=%ci HEAD@{1} 2>/dev/null || echo "unknown" ``` -The last command gets the date of the previous HEAD (before the update) to estimate version age. - -## 2. Write the event - -Write a JSON file to `/tmp/nanoclaw-diagnostics.json`. Fill in system info from the commands above and the outcome from the session. Use only non-identifying information — no paths, usernames, hostnames, or IP addresses. +Write `/tmp/nanoclaw-diagnostics.json`. No paths, usernames, hostnames, or IP addresses. ```json { "api_key": "phc_fx1Hhx9ucz8GuaJC8LVZWO8u03yXZZJJ6ObS4yplnaP", "event": "update_complete", - "distinct_id": "", + "distinct_id": "", "properties": { "success": true, "nanoclaw_version": "1.2.21", @@ -38,33 +32,18 @@ Write a JSON file to `/tmp/nanoclaw-diagnostics.json`. Fill in system info from } ``` -## 3. Show and ask - -Show the contents of the file to the user and ask: - -> "Would you like to send anonymous diagnostics to help improve NanoClaw? Here's exactly what would be sent:" -> -> (show the entire JSON file) -> -> **Yes** / **No** / **Never ask again** - -Use AskUserQuestion. - -## 4. Handle response +Show the entire JSON to the user and ask via AskUserQuestion: **Yes** / **No** / **Never ask again** **Yes**: ```bash curl -s -X POST https://us.i.posthog.com/capture/ -H 'Content-Type: application/json' -d @/tmp/nanoclaw-diagnostics.json rm /tmp/nanoclaw-diagnostics.json ``` -Confirm: "Diagnostics sent." -**No**: `rm /tmp/nanoclaw-diagnostics.json` — do nothing else. +**No**: `rm /tmp/nanoclaw-diagnostics.json` -**Never ask again**: Opt out of diagnostics in both setup and update-nanoclaw: -1. Replace the contents of `.claude/skills/setup/diagnostics.md` with just `# Diagnostics — opted out` -2. Replace the contents of `.claude/skills/update-nanoclaw/diagnostics.md` with just `# Diagnostics — opted out` -3. Remove the "Diagnostics (Optional)" section from the end of both `.claude/skills/setup/SKILL.md` and `.claude/skills/update-nanoclaw/SKILL.md` +**Never ask again**: +1. Replace contents of `.claude/skills/setup/diagnostics.md` with `# Diagnostics — opted out` +2. Replace contents of `.claude/skills/update-nanoclaw/diagnostics.md` with `# Diagnostics — opted out` +3. Remove the "Diagnostics" section from both `.claude/skills/setup/SKILL.md` and `.claude/skills/update-nanoclaw/SKILL.md` 4. `rm /tmp/nanoclaw-diagnostics.json` - -Confirm: "Got it — you won't be asked again."