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) <noreply@anthropic.com>
This commit is contained in:
99
.claude/skills/_shared/diagnostics.md
Normal file
99
.claude/skills/_shared/diagnostics.md
Normal file
@@ -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 <event_type> --success --data '<json>' --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 <event_type> --success --data '<json>'
|
||||||
|
```
|
||||||
|
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."
|
||||||
@@ -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.
|
- **"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.
|
- **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.
|
- **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`.
|
||||||
|
|||||||
@@ -201,3 +201,8 @@ The Discord bot supports:
|
|||||||
- @mention translation (Discord `<@botId>` → NanoClaw trigger format)
|
- @mention translation (Discord `<@botId>` → NanoClaw trigger format)
|
||||||
- Message splitting for responses over 2000 characters
|
- Message splitting for responses over 2000 characters
|
||||||
- Typing indicators while the agent processes
|
- Typing indicators while the agent processes
|
||||||
|
|
||||||
|
|
||||||
|
## Diagnostics (Optional)
|
||||||
|
|
||||||
|
After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`.
|
||||||
|
|||||||
@@ -218,3 +218,8 @@ npx -y @gongrzhe/server-gmail-autoauth-mcp
|
|||||||
6. Rebuild and restart
|
6. Rebuild and restart
|
||||||
7. Clear stale agent-runner copies: `rm -r data/sessions/*/agent-runner-src 2>/dev/null || true`
|
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)
|
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`.
|
||||||
|
|||||||
@@ -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 - 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.
|
- **"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.
|
- **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`.
|
||||||
|
|||||||
@@ -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
|
### 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: ..."
|
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`.
|
||||||
|
|||||||
@@ -288,3 +288,8 @@ To remove Parallel AI integration:
|
|||||||
3. Remove Web Research Tools section from groups/main/CLAUDE.md
|
3. Remove Web Research Tools section from groups/main/CLAUDE.md
|
||||||
4. Rebuild: `./container/build.sh && npm run build`
|
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)
|
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`.
|
||||||
|
|||||||
@@ -102,3 +102,8 @@ The PDF may be scanned (image-based). pdftotext only handles text-based PDFs. Co
|
|||||||
### WhatsApp PDF not detected
|
### 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.
|
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`.
|
||||||
|
|||||||
@@ -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
|
- 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
|
- Verify WhatsApp is connected: check logs for connection status
|
||||||
|
|
||||||
|
|
||||||
|
## Diagnostics (Optional)
|
||||||
|
|
||||||
|
After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.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.
|
- **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.
|
- **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.
|
- **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`.
|
||||||
|
|||||||
@@ -382,3 +382,8 @@ To remove Agent Swarm support while keeping basic Telegram:
|
|||||||
6. Remove Agent Teams section from group CLAUDE.md files
|
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
|
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)
|
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`.
|
||||||
|
|||||||
@@ -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:%'"`
|
4. Remove Telegram registrations from SQLite: `sqlite3 store/messages.db "DELETE FROM registered_groups WHERE jid LIKE 'tg:%'"`
|
||||||
5. Uninstall: `npm uninstall grammy`
|
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)
|
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`.
|
||||||
|
|||||||
@@ -146,3 +146,8 @@ Check logs for the specific error. Common causes:
|
|||||||
### Agent doesn't respond to voice notes
|
### Agent doesn't respond to voice notes
|
||||||
|
|
||||||
Verify the chat is registered and the agent is running. Voice transcription only runs for registered groups.
|
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`.
|
||||||
|
|||||||
@@ -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'"`
|
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`
|
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)
|
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`.
|
||||||
|
|||||||
@@ -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 |
|
| `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/Dockerfile` | Entrypoint: `mount --bind` for .env shadowing, `setpriv` privilege drop |
|
||||||
| `container/build.sh` | Default runtime: `docker` → `container` |
|
| `container/build.sh` | Default runtime: `docker` → `container` |
|
||||||
|
|
||||||
|
|
||||||
|
## Diagnostics (Optional)
|
||||||
|
|
||||||
|
After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.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`)
|
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`
|
4. Add the channel to `main()` in `src/index.ts`
|
||||||
5. Tell user how to authenticate and test
|
5. Tell user how to authenticate and test
|
||||||
|
|
||||||
|
|
||||||
|
## Diagnostics (Optional)
|
||||||
|
|
||||||
|
After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.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=$(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"
|
[ "$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`.
|
||||||
|
|||||||
@@ -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
|
- **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 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
|
- **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`.
|
||||||
|
|||||||
@@ -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.
|
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.
|
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`.
|
||||||
|
|||||||
@@ -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.
|
**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`
|
**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`.
|
||||||
|
|||||||
@@ -233,3 +233,8 @@ Tell the user:
|
|||||||
- Restart the service to apply changes:
|
- Restart the service to apply changes:
|
||||||
- If using launchd: `launchctl unload ~/Library/LaunchAgents/com.nanoclaw.plist && launchctl load ~/Library/LaunchAgents/com.nanoclaw.plist`
|
- If using launchd: `launchctl unload ~/Library/LaunchAgents/com.nanoclaw.plist && launchctl load ~/Library/LaunchAgents/com.nanoclaw.plist`
|
||||||
- If running manually: restart `npm run dev`
|
- If running manually: restart `npm run dev`
|
||||||
|
|
||||||
|
|
||||||
|
## Diagnostics (Optional)
|
||||||
|
|
||||||
|
After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`.
|
||||||
|
|||||||
@@ -128,3 +128,8 @@ Show:
|
|||||||
- Any conflicts that were resolved (list files)
|
- Any conflicts that were resolved (list files)
|
||||||
|
|
||||||
If the service is running, remind the user to restart it to pick up changes.
|
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`.
|
||||||
|
|||||||
@@ -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.
|
**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`.
|
**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`.
|
||||||
|
|||||||
@@ -415,3 +415,7 @@ docker run nanoclaw-agent ls -la /app/src/skills/
|
|||||||
- `data/x-auth.json` - Auth state marker (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`)
|
- Only main group can use X tools (enforced in `agent.ts` and `host.ts`)
|
||||||
- Scripts run as subprocesses with limited environment
|
- Scripts run as subprocesses with limited environment
|
||||||
|
|
||||||
|
## Diagnostics (Optional)
|
||||||
|
|
||||||
|
After completing all steps above, read and follow `.claude/skills/_shared/diagnostics.md`.
|
||||||
|
|||||||
310
scripts/send-diagnostics.ts
Normal file
310
scripts/send-diagnostics.ts
Normal file
@@ -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 <setup_complete|skill_applied|update_complete> \
|
||||||
|
* [--success|--failure] \
|
||||||
|
* [--data '<json>'] \
|
||||||
|
* [--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<string, unknown>;
|
||||||
|
dryRun: boolean;
|
||||||
|
} {
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
let event = '';
|
||||||
|
let success: boolean | undefined;
|
||||||
|
let data: Record<string, unknown> = {};
|
||||||
|
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<string, unknown> {
|
||||||
|
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<string, unknown> {
|
||||||
|
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<string> | 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<string, unknown>): 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<string, unknown>,
|
||||||
|
eventData: Record<string, unknown>,
|
||||||
|
success?: boolean,
|
||||||
|
): Record<string, unknown> {
|
||||||
|
const properties: Record<string, unknown> = {
|
||||||
|
$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<string, unknown>,
|
||||||
|
): Promise<void> {
|
||||||
|
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<void> {
|
||||||
|
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();
|
||||||
Reference in New Issue
Block a user