From 5a4de7f83b8f7a7f2b38b9dd992801ebada5300e Mon Sep 17 00:00:00 2001 From: David Li Date: Wed, 13 May 2026 15:50:22 +0800 Subject: [PATCH] chore: auto-commit after worktree-switch GSD-Unit: m001 --- .gitignore | 27 + .gsd.migrating/CODEBASE.md | 86 ++ .gsd.migrating/CONTEXT.md | 16 + .gsd.migrating/DECISIONS.md | 12 + .gsd.migrating/PREFERENCES.md | 16 + .gsd.migrating/PROJECT.md | 35 + .gsd.migrating/REQUIREMENTS.md | 163 ++++ .gsd.migrating/STATE.md | 18 + .gsd.migrating/audit/events.jsonl | 29 + .gsd.migrating/auto.lock | 7 + .gsd.migrating/event-log.jsonl | 1 + .../milestones/M001/M001-CONTEXT.md | 169 ++++ .../milestones/M001/M001-ROADMAP.md | 21 + .gsd.migrating/notifications.jsonl | 31 + .gsd.migrating/state-manifest.json | 160 ++++ notes/ARCH.md | 455 ++++++++++ notes/TCP.md | 847 ++++++++++++++++++ 17 files changed, 2093 insertions(+) create mode 100644 .gsd.migrating/CODEBASE.md create mode 100644 .gsd.migrating/CONTEXT.md create mode 100644 .gsd.migrating/DECISIONS.md create mode 100644 .gsd.migrating/PREFERENCES.md create mode 100644 .gsd.migrating/PROJECT.md create mode 100644 .gsd.migrating/REQUIREMENTS.md create mode 100644 .gsd.migrating/STATE.md create mode 100644 .gsd.migrating/audit/events.jsonl create mode 100644 .gsd.migrating/auto.lock create mode 100644 .gsd.migrating/event-log.jsonl create mode 100644 .gsd.migrating/milestones/M001/M001-CONTEXT.md create mode 100644 .gsd.migrating/milestones/M001/M001-ROADMAP.md create mode 100644 .gsd.migrating/notifications.jsonl create mode 100644 .gsd.migrating/state-manifest.json create mode 100644 notes/ARCH.md create mode 100644 notes/TCP.md diff --git a/.gitignore b/.gitignore index cf83214..6be0eba 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,30 @@ __pycache__/ Thumbs.db find_all_keys_macos .claude/worktrees/ + +# ── GSD baseline (auto-generated) ── +.gsd +.gsd-id +.mcp.json +.bg-shell/ +*.swp +*.swo +*~ +.idea/ +.vscode/ +*.code-workspace +.env +.env.* +!.env.example +node_modules/ +.next/ +dist/ +build/ +*.pyc +.venv/ +venv/ +vendor/ +*.log +coverage/ +.cache/ +tmp/ diff --git a/.gsd.migrating/CODEBASE.md b/.gsd.migrating/CODEBASE.md new file mode 100644 index 0000000..09a11fc --- /dev/null +++ b/.gsd.migrating/CODEBASE.md @@ -0,0 +1,86 @@ +# Codebase Map + +Generated: 2026-05-13T05:22:55Z | Files: 52 | Described: 0/52 + + +### (root)/ +- `.gitignore` +- `AGENTS.md` +- `Cargo.toml` +- `CLAUDE.md` +- `config.example.json` +- `install.ps1` +- `install.sh` +- `LICENSE` +- `README.md` +- `SKILL.md` + +### .github/workflows/ +- `.github/workflows/release.yml` + +### docs/ +- `docs/macos-3x-vs-4x-decryption-guide.md` +- `docs/macos-permission-guide.md` + +### npm/platforms/darwin-arm64/ +- `npm/platforms/darwin-arm64/package.json` + +### npm/platforms/darwin-x64/ +- `npm/platforms/darwin-x64/package.json` + +### npm/platforms/linux-arm64/ +- `npm/platforms/linux-arm64/package.json` + +### npm/platforms/linux-x64/ +- `npm/platforms/linux-x64/package.json` + +### npm/platforms/win32-x64/ +- `npm/platforms/win32-x64/package.json` + +### npm/wx-cli/ +- `npm/wx-cli/install.js` +- `npm/wx-cli/package.json` + +### npm/wx-cli/bin/ +- `npm/wx-cli/bin/wx.js` + +### src/ +- `src/config.rs` +- `src/ipc.rs` +- `src/main.rs` + +### src/cli/ +- `src/cli/contacts.rs` +- `src/cli/daemon_cmd.rs` +- `src/cli/export.rs` +- `src/cli/favorites.rs` +- `src/cli/history.rs` +- `src/cli/init.rs` +- `src/cli/members.rs` +- `src/cli/mod.rs` +- `src/cli/new_messages.rs` +- `src/cli/output.rs` +- `src/cli/search.rs` +- `src/cli/sessions.rs` +- `src/cli/sns_feed.rs` +- `src/cli/sns_notifications.rs` +- `src/cli/sns_search.rs` +- `src/cli/stats.rs` +- `src/cli/transport.rs` +- `src/cli/unread.rs` + +### src/crypto/ +- `src/crypto/mod.rs` +- `src/crypto/wal.rs` + +### src/daemon/ +- `src/daemon/cache.rs` +- `src/daemon/mod.rs` +- `src/daemon/query.rs` +- `src/daemon/server.rs` + +### src/scanner/ +- `src/scanner/linux.rs` +- `src/scanner/macos.rs` +- `src/scanner/mod.rs` +- `src/scanner/windows.rs` diff --git a/.gsd.migrating/CONTEXT.md b/.gsd.migrating/CONTEXT.md new file mode 100644 index 0000000..8ef65c5 --- /dev/null +++ b/.gsd.migrating/CONTEXT.md @@ -0,0 +1,16 @@ +# Project Context + +Auto-detected by GSD init wizard. Edit or expand as needed. + +## Language / Stack + +Primary: rust + +## Project Files + +- Cargo.toml +- .github/workflows + +## CI/CD + +CI configuration detected. diff --git a/.gsd.migrating/DECISIONS.md b/.gsd.migrating/DECISIONS.md new file mode 100644 index 0000000..b1dd9b1 --- /dev/null +++ b/.gsd.migrating/DECISIONS.md @@ -0,0 +1,12 @@ +# Decisions Register + + + +| # | When | Scope | Decision | Choice | Rationale | Revisable? | Made By | +|---|------|-------|----------|--------|-----------|------------|---------| +| D001 | | architecture | Transport abstraction via traits | Listener and Connector traits with shared protocol.rs, implementations for Unix/Windows/TCP | Eliminates ~50 lines of duplicated JSON-line protocol handling, provides clear extension point for future transports | Yes | collaborative | +| D002 | | architecture | Global --tcp CLI flag for transport selection | Global clap flag on root Cli struct, inherited by all subcommands | Discoverable, consistent UX. User specifies once, affects all commands | Yes | human | +| D003 | | architecture | No built-in TCP security | No TLS, no auth tokens, no IP whitelist in this milestone. Bind exactly as user specifies. | User handles firewall/ACL at OS level. TLS adds cert management and dependency complexity. Can be added later non-breaking. | Yes | collaborative | +| D004 | | architecture | One request per connection protocol model | One JSON-line request per connection, no keepalive or pooling | Matches existing behavior, minimal complexity, sufficient for CLI usage patterns | Yes | agent | diff --git a/.gsd.migrating/PREFERENCES.md b/.gsd.migrating/PREFERENCES.md new file mode 100644 index 0000000..5b4887f --- /dev/null +++ b/.gsd.migrating/PREFERENCES.md @@ -0,0 +1,16 @@ +--- +version: 1 +mode: solo +git: + isolation: worktree + main_branch: main + auto_push: true +verification_commands: + - cargo test + - cargo clippy +--- +# GSD Project Preferences + +Generated by `/gsd init`. Edit directly or use `/gsd prefs project` to modify. + +See `~/.gsd/agent/extensions/gsd/docs/preferences-reference.md` for full field documentation. diff --git a/.gsd.migrating/PROJECT.md b/.gsd.migrating/PROJECT.md new file mode 100644 index 0000000..25adf60 --- /dev/null +++ b/.gsd.migrating/PROJECT.md @@ -0,0 +1,35 @@ +# wx-cli + +## What This Is + +A cross-platform Rust CLI tool for extracting and querying local WeChat 4.x data. Decrypts SQLCipher-encrypted databases, caches decrypted copies with mtime-aware invalidation, and provides a daemon-based IPC architecture for fast repeated queries. Currently uses Unix sockets (macOS/Linux) and Windows named pipes for local-only communication. + +## Core Value + +Query your local WeChat chat history, contacts, and moments from the command line with millisecond response times — data never leaves your machine. + +## Project Shape + +- **Complexity:** simple +- **Why:** Well-defined scope, existing codebase with clear module boundaries, single transport addition with refactoring + +## Current State + +Version 0.1.10. Fully functional CLI with 17 subcommands. Daemon auto-starts on first query. Cross-platform (macOS, Linux, Windows). No integration tests. Local IPC only. + +## Architecture / Key Patterns + +- Single binary: client and daemon (`WX_DAEMON_MODE` env var) +- Daemon uses tokio async runtime, Unix socket / Windows named pipe IPC +- JSON-line protocol: one request per connection +- mtime-aware decryption cache in `~/.wx-cli/cache/` +- Platform-specific memory scanners for SQLCipher key extraction +- All queries executed via rusqlite on decrypted DBs + +## Capability Contract + +See `.gsd/REQUIREMENTS.md` for the explicit capability contract. + +## Milestone Sequence + +- [ ] M001: TCP Transport — Add `--tcp host:port` global flag and TCP transport support to daemon and client \ No newline at end of file diff --git a/.gsd.migrating/REQUIREMENTS.md b/.gsd.migrating/REQUIREMENTS.md new file mode 100644 index 0000000..8f444ab --- /dev/null +++ b/.gsd.migrating/REQUIREMENTS.md @@ -0,0 +1,163 @@ +# Requirements + +## Active + +### R001 — TCP transport on server +- Class: core-capability +- Status: active +- Description: Daemon listens on TCP when `--tcp host:port` is specified, in addition to local transport +- Why it matters: Enables remote clients to query WeChat data over network +- Source: user +- Primary owning slice: M001/S01 +- Supporting slices: M001/S02 +- Validation: unmapped +- Notes: Bind exactly as user specifies, no TLS, no IP whitelist + +### R002 — TCP transport on client +- Class: core-capability +- Status: active +- Description: Client connects via TCP when `--tcp host:port` is specified, with no local fallback +- Why it matters: Users explicitly choosing TCP must connect to that address +- Source: user +- Primary owning slice: M001/S02 +- Supporting slices: none +- Validation: unmapped +- Notes: Hard error if connection fails, no silent fallback + +### R003 — Transport abstraction layer +- Class: quality-attribute +- Status: active +- Description: Transport layer uses trait-based abstraction (Listener/Connector) to eliminate platform duplication +- Why it matters: Makes adding new transports (TCP, future TLS) easy without duplicating protocol logic +- Source: inferred +- Primary owning slice: M001/S01 +- Supporting slices: M001/S02, M001/S03 +- Validation: unmapped +- Notes: Must support Unix socket, Windows named pipe, and TCP + +### R004 — Global `--tcp` CLI flag +- Class: primary-user-loop +- Status: active +- Description: `--tcp host:port` is a global CLI flag, affecting all commands including `daemon status`, `daemon logs`, and all query commands +- Why it matters: Discoverable, consistent interface for TCP across all commands +- Source: user +- Primary owning slice: M001/S02 +- Supporting slices: none +- Validation: unmapped +- Notes: Replaces env var approach, cleaner UX + +### R005 — Daemon start command +- Class: primary-user-loop +- Status: active +- Description: New `wx daemon start` subcommand to explicitly start the daemon with configurable options +- Why it matters: Currently daemon auto-starts on first query; explicit start gives user control over transport config +- Source: user +- Primary owning slice: M001/S01 +- Supporting slices: none +- Validation: unmapped +- Notes: Should support `--tcp` flag + +### R006 — Cross-platform compilation +- Class: constraint +- Status: active +- Description: Code compiles on macOS, Linux, and Windows (`cargo check` on all targets) +- Why it matters: Project is cross-platform by design, TCP must work on all three +- Source: inferred +- Primary owning slice: M001/S01 +- Supporting slices: M001/S02, M001/S03 +- Validation: unmapped +- Notes: TcpListener/TcpStream are std library, should be trivial + +### R007 — Error handling for TCP failures +- Class: failure-visibility +- Status: active +- Description: TCP bind/connect failures produce clear error messages with no silent fallback +- Why it matters: Users need to know when transport configuration fails +- Source: inferred +- Primary owning slice: M001/S02 +- Supporting slices: none +- Validation: unmapped +- Notes: 15s connect timeout, 120s read/write timeout + +### R008 — Integration: CLI ↔ daemon over TCP +- Class: integration +- Status: active +- Description: End-to-end verification: CLI and daemon communicate successfully over TCP on localhost +- Why it matters: Proves the transport actually works, not just compiles +- Source: inferred +- Primary owning slice: M001/S04 +- Supporting slices: none +- Validation: unmapped +- Notes: Manual smoke test sufficient, no automated integration tests + +## Deferred + +### R020 — TLS encryption for TCP transport +- Class: compliance/security +- Status: deferred +- Description: Optional TLS encryption on TCP transport for secure remote access +- Why it matters: Plaintext TCP exposes chat data to network sniffing +- Source: inferred +- Primary owning slice: none +- Supporting slices: none +- Validation: unmapped +- Notes: Deferred — adds tokio-rustls dependency and cert management complexity + +### R021 — Authentication tokens for TCP +- Class: compliance/security +- Status: deferred +- Description: Token-based authentication for TCP connections +- Why it matters: Prevents unauthorized access to WeChat data over network +- Source: inferred +- Primary owning slice: none +- Supporting slices: none +- Validation: unmapped +- Notes: Deferred — requires protocol change (Auth request type) + +### R022 — TCP connection keepalive +- Class: quality-attribute +- Status: deferred +- Description: Persistent TCP connections with keepalive for reduced latency +- Why it matters: Current one-request-per-connection model has connection overhead +- Source: inferred +- Primary owning slice: none +- Supporting slices: none +- Validation: unmapped +- Notes: Deferred — requires protocol and connection management changes + +## Out of Scope + +### R030 — Network-level access control +- Class: constraint +- Status: out-of-scope +- Description: IP whitelisting, firewall rules, or network ACLs within the application +- Why it matters: Prevents scope creep into network security management +- Source: user +- Primary owning slice: none +- Supporting slices: none +- Validation: n/a +- Notes: User handles firewall/ACL at OS level + +## Traceability + +| ID | Class | Status | Primary owner | Supporting | Proof | +|---|---|---|---|---|---| +| R001 | core-capability | active | M001/S01 | M001/S02 | unmapped | +| R002 | core-capability | active | M001/S02 | none | unmapped | +| R003 | quality-attribute | active | M001/S01 | M001/S02, M001/S03 | unmapped | +| R004 | primary-user-loop | active | M001/S02 | none | unmapped | +| R005 | primary-user-loop | active | M001/S01 | none | unmapped | +| R006 | constraint | active | M001/S01 | M001/S02, M001/S03 | unmapped | +| R007 | failure-visibility | active | M001/S02 | none | unmapped | +| R008 | integration | active | M001/S04 | none | unmapped | +| R020 | compliance/security | deferred | none | none | unmapped | +| R021 | compliance/security | deferred | none | none | unmapped | +| R022 | quality-attribute | deferred | none | none | unmapped | +| R030 | constraint | out-of-scope | none | none | n/a | + +## Coverage Summary + +- Active requirements: 8 +- Mapped to slices: 8 +- Validated: 0 +- Unmapped active requirements: 0 \ No newline at end of file diff --git a/.gsd.migrating/STATE.md b/.gsd.migrating/STATE.md new file mode 100644 index 0000000..83858eb --- /dev/null +++ b/.gsd.migrating/STATE.md @@ -0,0 +1,18 @@ +# GSD State + +**Active Milestone:** M001: TCP Transport +**Active Slice:** S01: Transport abstraction layer +**Phase:** planning +**Requirements Status:** 0 active · 0 validated · 0 deferred · 0 out of scope + +## Milestone Registry +- 🔄 **M001:** TCP Transport + +## Recent Decisions +- None recorded + +## Blockers +- None + +## Next Action +Slice S01 has no DB tasks. Plan slice tasks before execution. diff --git a/.gsd.migrating/audit/events.jsonl b/.gsd.migrating/audit/events.jsonl new file mode 100644 index 0000000..e0aa932 --- /dev/null +++ b/.gsd.migrating/audit/events.jsonl @@ -0,0 +1,29 @@ +{"version":"1","eventId":"35a18d2d-ef3f-4957-8fa0-e018c9091b9b","traceId":"model:59b3413b-c62a-487e-acdb-3c4eddecc0a3:1778648805290","turnId":"discuss-milestone:","category":"model-policy","type":"model-policy-allow","ts":"2026-05-13T05:06:45.292Z","payload":{"modelId":"claude-haiku-4.5","provider":"github-copilot","api":"anthropic-messages","reason":"allowed","unitType":"discuss-milestone","requirements":{"reasoning":0.6,"instruction":0.7}}} +{"version":"1","eventId":"bab2980a-198e-447a-a9aa-dd1e5d508edb","traceId":"model:59b3413b-c62a-487e-acdb-3c4eddecc0a3:1778648805290","turnId":"discuss-milestone:","category":"model-policy","type":"model-policy-allow","ts":"2026-05-13T05:06:45.298Z","payload":{"modelId":"claude-sonnet-4.6","provider":"github-copilot","api":"anthropic-messages","reason":"allowed","unitType":"discuss-milestone","requirements":{"reasoning":0.6,"instruction":0.7}}} +{"version":"1","eventId":"54bb4aac-5585-47cc-9589-dec03ac30905","traceId":"model:59b3413b-c62a-487e-acdb-3c4eddecc0a3:1778648805290","turnId":"discuss-milestone:","category":"model-policy","type":"model-policy-allow","ts":"2026-05-13T05:06:45.300Z","payload":{"modelId":"gemini-3.1-pro-preview","provider":"github-copilot","api":"openai-completions","reason":"allowed","unitType":"discuss-milestone","requirements":{"reasoning":0.6,"instruction":0.7}}} +{"version":"1","eventId":"05db147d-4818-4a9d-be76-1945e659b5c8","traceId":"model:59b3413b-c62a-487e-acdb-3c4eddecc0a3:1778648805290","turnId":"discuss-milestone:","category":"model-policy","type":"model-policy-allow","ts":"2026-05-13T05:06:45.303Z","payload":{"modelId":"gpt-4.1","provider":"github-copilot","api":"openai-completions","reason":"allowed","unitType":"discuss-milestone","requirements":{"reasoning":0.6,"instruction":0.7}}} +{"version":"1","eventId":"1a145816-1b1d-4d84-8d1d-6a08c629e27c","traceId":"model:59b3413b-c62a-487e-acdb-3c4eddecc0a3:1778648805290","turnId":"discuss-milestone:","category":"model-policy","type":"model-policy-allow","ts":"2026-05-13T05:06:45.305Z","payload":{"modelId":"gpt-4o","provider":"github-copilot","api":"openai-completions","reason":"allowed","unitType":"discuss-milestone","requirements":{"reasoning":0.6,"instruction":0.7}}} +{"version":"1","eventId":"dc301e36-8e87-4b0e-9a7e-64cbc907480c","traceId":"model:59b3413b-c62a-487e-acdb-3c4eddecc0a3:1778648805290","turnId":"discuss-milestone:","category":"model-policy","type":"model-policy-allow","ts":"2026-05-13T05:06:45.308Z","payload":{"modelId":"gpt-5-mini","provider":"github-copilot","api":"openai-responses","reason":"allowed","unitType":"discuss-milestone","requirements":{"reasoning":0.6,"instruction":0.7}}} +{"version":"1","eventId":"99fca380-cec0-412e-ba94-7b16c5496616","traceId":"model:59b3413b-c62a-487e-acdb-3c4eddecc0a3:1778648805290","turnId":"discuss-milestone:","category":"model-policy","type":"model-policy-allow","ts":"2026-05-13T05:06:45.310Z","payload":{"modelId":"gpt-5.2-codex","provider":"github-copilot","api":"openai-responses","reason":"allowed","unitType":"discuss-milestone","requirements":{"reasoning":0.6,"instruction":0.7}}} +{"version":"1","eventId":"45fb32d0-10b3-43e6-9f4e-1a12eb2782d3","traceId":"model:59b3413b-c62a-487e-acdb-3c4eddecc0a3:1778648805290","turnId":"discuss-milestone:","category":"model-policy","type":"model-policy-allow","ts":"2026-05-13T05:06:45.313Z","payload":{"modelId":"gpt-5.4","provider":"github-copilot","api":"openai-responses","reason":"allowed","unitType":"discuss-milestone","requirements":{"reasoning":0.6,"instruction":0.7}}} +{"version":"1","eventId":"996a6fef-9644-4881-be02-c3dffbfd5cf0","traceId":"model:59b3413b-c62a-487e-acdb-3c4eddecc0a3:1778648805290","turnId":"discuss-milestone:","category":"model-policy","type":"model-policy-allow","ts":"2026-05-13T05:06:45.315Z","payload":{"modelId":"gpt-5.4-mini","provider":"github-copilot","api":"openai-responses","reason":"allowed","unitType":"discuss-milestone","requirements":{"reasoning":0.6,"instruction":0.7}}} +{"version":"1","eventId":"cf87b34b-c148-4eda-b6df-f688da404221","traceId":"model:59b3413b-c62a-487e-acdb-3c4eddecc0a3:1778648805290","turnId":"discuss-milestone:","category":"model-policy","type":"model-policy-allow","ts":"2026-05-13T05:06:45.318Z","payload":{"modelId":"grok-code-fast-1","provider":"github-copilot","api":"openai-completions","reason":"allowed","unitType":"discuss-milestone","requirements":{"reasoning":0.6,"instruction":0.7}}} +{"version":"1","eventId":"5b87f7ba-7eec-4bb4-900a-be5d84e0e5bd","traceId":"model:59b3413b-c62a-487e-acdb-3c4eddecc0a3:1778648805290","turnId":"discuss-milestone:","category":"model-policy","type":"model-policy-allow","ts":"2026-05-13T05:06:45.320Z","payload":{"modelId":"groq/compound","provider":"groq","api":"openai-completions","reason":"allowed","unitType":"discuss-milestone","requirements":{"reasoning":0.6,"instruction":0.7}}} +{"version":"1","eventId":"db19cee6-e80d-48fa-aa6e-199508e92d78","traceId":"model:59b3413b-c62a-487e-acdb-3c4eddecc0a3:1778648805290","turnId":"discuss-milestone:","category":"model-policy","type":"model-policy-allow","ts":"2026-05-13T05:06:45.323Z","payload":{"modelId":"groq/compound-mini","provider":"groq","api":"openai-completions","reason":"allowed","unitType":"discuss-milestone","requirements":{"reasoning":0.6,"instruction":0.7}}} +{"version":"1","eventId":"37e1b2cb-e7ad-4417-84ad-4f5c84ef5581","traceId":"model:59b3413b-c62a-487e-acdb-3c4eddecc0a3:1778648805290","turnId":"discuss-milestone:","category":"model-policy","type":"model-policy-allow","ts":"2026-05-13T05:06:45.325Z","payload":{"modelId":"llama-3.1-8b-instant","provider":"groq","api":"openai-completions","reason":"allowed","unitType":"discuss-milestone","requirements":{"reasoning":0.6,"instruction":0.7}}} +{"version":"1","eventId":"d03691e7-7a88-45a1-85db-889e3e99a183","traceId":"model:59b3413b-c62a-487e-acdb-3c4eddecc0a3:1778648805290","turnId":"discuss-milestone:","category":"model-policy","type":"model-policy-allow","ts":"2026-05-13T05:06:45.328Z","payload":{"modelId":"llama-3.3-70b-versatile","provider":"groq","api":"openai-completions","reason":"allowed","unitType":"discuss-milestone","requirements":{"reasoning":0.6,"instruction":0.7}}} +{"version":"1","eventId":"d956e2fd-0780-4d8a-891a-0f78cf4fb885","traceId":"model:59b3413b-c62a-487e-acdb-3c4eddecc0a3:1778648805290","turnId":"discuss-milestone:","category":"model-policy","type":"model-policy-allow","ts":"2026-05-13T05:06:45.330Z","payload":{"modelId":"meta-llama/llama-4-scout-17b-16e-instruct","provider":"groq","api":"openai-completions","reason":"allowed","unitType":"discuss-milestone","requirements":{"reasoning":0.6,"instruction":0.7}}} +{"version":"1","eventId":"c96af6c7-6ab8-49f7-9b2e-63ccc3b679be","traceId":"model:59b3413b-c62a-487e-acdb-3c4eddecc0a3:1778648805290","turnId":"discuss-milestone:","category":"model-policy","type":"model-policy-allow","ts":"2026-05-13T05:06:45.332Z","payload":{"modelId":"openai/gpt-oss-120b","provider":"groq","api":"openai-completions","reason":"allowed","unitType":"discuss-milestone","requirements":{"reasoning":0.6,"instruction":0.7}}} +{"version":"1","eventId":"1162b48d-9843-48ba-91db-58cf417d2b69","traceId":"model:59b3413b-c62a-487e-acdb-3c4eddecc0a3:1778648805290","turnId":"discuss-milestone:","category":"model-policy","type":"model-policy-allow","ts":"2026-05-13T05:06:45.335Z","payload":{"modelId":"openai/gpt-oss-20b","provider":"groq","api":"openai-completions","reason":"allowed","unitType":"discuss-milestone","requirements":{"reasoning":0.6,"instruction":0.7}}} +{"version":"1","eventId":"0eb90b03-3888-46a8-82d0-e7ec7ed69c92","traceId":"model:59b3413b-c62a-487e-acdb-3c4eddecc0a3:1778648805290","turnId":"discuss-milestone:","category":"model-policy","type":"model-policy-allow","ts":"2026-05-13T05:06:45.337Z","payload":{"modelId":"openai/gpt-oss-safeguard-20b","provider":"groq","api":"openai-completions","reason":"allowed","unitType":"discuss-milestone","requirements":{"reasoning":0.6,"instruction":0.7}}} +{"version":"1","eventId":"c1fd584d-bfdd-4245-a351-d81f8961474d","traceId":"model:59b3413b-c62a-487e-acdb-3c4eddecc0a3:1778648805290","turnId":"discuss-milestone:","category":"model-policy","type":"model-policy-allow","ts":"2026-05-13T05:06:45.340Z","payload":{"modelId":"qwen/qwen3-32b","provider":"groq","api":"openai-completions","reason":"allowed","unitType":"discuss-milestone","requirements":{"reasoning":0.6,"instruction":0.7}}} +{"version":"1","eventId":"ef3e9541-3dfe-4290-a5d0-ca5094a2a4ee","traceId":"model:59b3413b-c62a-487e-acdb-3c4eddecc0a3:1778648805290","turnId":"discuss-milestone:","category":"model-policy","type":"model-policy-allow","ts":"2026-05-13T05:06:45.343Z","payload":{"modelId":"llm-p710","provider":"p710","api":"openai-completions","reason":"allowed","unitType":"discuss-milestone","requirements":{"reasoning":0.6,"instruction":0.7}}} +{"version":"1","eventId":"988b326e-91d3-48ea-9f73-b90a60536d94","traceId":"model:59b3413b-c62a-487e-acdb-3c4eddecc0a3:1778648805290","turnId":"discuss-milestone:","category":"model-policy","type":"model-policy-allow","ts":"2026-05-13T05:06:45.345Z","payload":{"modelId":"qwen3.6-plus","provider":"bailian","api":"openai-completions","reason":"allowed","unitType":"discuss-milestone","requirements":{"reasoning":0.6,"instruction":0.7}}} +{"version":"1","eventId":"0a79bcf8-24ab-4f40-a1f5-f3c3ee43928f","traceId":"model:59b3413b-c62a-487e-acdb-3c4eddecc0a3:1778648805290","turnId":"discuss-milestone:","category":"model-policy","type":"model-policy-allow","ts":"2026-05-13T05:06:45.347Z","payload":{"modelId":"qwen3.5-plus","provider":"bailian","api":"openai-completions","reason":"allowed","unitType":"discuss-milestone","requirements":{"reasoning":0.6,"instruction":0.7}}} +{"version":"1","eventId":"b927da73-de27-4d01-be2a-9bbdeed206b6","traceId":"model:59b3413b-c62a-487e-acdb-3c4eddecc0a3:1778648805290","turnId":"discuss-milestone:","category":"model-policy","type":"model-policy-allow","ts":"2026-05-13T05:06:45.350Z","payload":{"modelId":"qwen3-max-2026-01-23","provider":"bailian","api":"openai-completions","reason":"allowed","unitType":"discuss-milestone","requirements":{"reasoning":0.6,"instruction":0.7}}} +{"version":"1","eventId":"06f49f12-432f-45e5-a5cf-8d0c4b9e4630","traceId":"model:59b3413b-c62a-487e-acdb-3c4eddecc0a3:1778648805290","turnId":"discuss-milestone:","category":"model-policy","type":"model-policy-allow","ts":"2026-05-13T05:06:45.352Z","payload":{"modelId":"qwen3-coder-next","provider":"bailian","api":"openai-completions","reason":"allowed","unitType":"discuss-milestone","requirements":{"reasoning":0.6,"instruction":0.7}}} +{"version":"1","eventId":"3b2834be-fa85-44e5-828d-7886bb24a1f5","traceId":"model:59b3413b-c62a-487e-acdb-3c4eddecc0a3:1778648805290","turnId":"discuss-milestone:","category":"model-policy","type":"model-policy-allow","ts":"2026-05-13T05:06:45.355Z","payload":{"modelId":"qwen3-coder-plus","provider":"bailian","api":"openai-completions","reason":"allowed","unitType":"discuss-milestone","requirements":{"reasoning":0.6,"instruction":0.7}}} +{"version":"1","eventId":"48ad4516-199b-48a3-aa1e-308a90de7d01","traceId":"model:59b3413b-c62a-487e-acdb-3c4eddecc0a3:1778648805290","turnId":"discuss-milestone:","category":"model-policy","type":"model-policy-allow","ts":"2026-05-13T05:06:45.357Z","payload":{"modelId":"MiniMax-M2.5","provider":"bailian","api":"openai-completions","reason":"allowed","unitType":"discuss-milestone","requirements":{"reasoning":0.6,"instruction":0.7}}} +{"version":"1","eventId":"d14885db-88a6-4c0f-9f25-d9a03e5002de","traceId":"model:59b3413b-c62a-487e-acdb-3c4eddecc0a3:1778648805290","turnId":"discuss-milestone:","category":"model-policy","type":"model-policy-allow","ts":"2026-05-13T05:06:45.360Z","payload":{"modelId":"glm-5","provider":"bailian","api":"openai-completions","reason":"allowed","unitType":"discuss-milestone","requirements":{"reasoning":0.6,"instruction":0.7}}} +{"version":"1","eventId":"39852010-4285-4fec-81c7-db5578eb845e","traceId":"model:59b3413b-c62a-487e-acdb-3c4eddecc0a3:1778648805290","turnId":"discuss-milestone:","category":"model-policy","type":"model-policy-allow","ts":"2026-05-13T05:06:45.362Z","payload":{"modelId":"glm-4.7","provider":"bailian","api":"openai-completions","reason":"allowed","unitType":"discuss-milestone","requirements":{"reasoning":0.6,"instruction":0.7}}} +{"version":"1","eventId":"84dc0529-e1d2-483c-81ef-edb89fcbba8f","traceId":"model:59b3413b-c62a-487e-acdb-3c4eddecc0a3:1778648805290","turnId":"discuss-milestone:","category":"model-policy","type":"model-policy-allow","ts":"2026-05-13T05:06:45.364Z","payload":{"modelId":"kimi-k2.5","provider":"bailian","api":"openai-completions","reason":"allowed","unitType":"discuss-milestone","requirements":{"reasoning":0.6,"instruction":0.7}}} diff --git a/.gsd.migrating/auto.lock b/.gsd.migrating/auto.lock new file mode 100644 index 0000000..67f7391 --- /dev/null +++ b/.gsd.migrating/auto.lock @@ -0,0 +1,7 @@ +{ + "pid": 42440, + "startedAt": "2026-05-13T05:32:05.812Z", + "unitType": "starting", + "unitId": "bootstrap", + "unitStartedAt": "2026-05-13T05:32:05.812Z" +} \ No newline at end of file diff --git a/.gsd.migrating/event-log.jsonl b/.gsd.migrating/event-log.jsonl new file mode 100644 index 0000000..9888d44 --- /dev/null +++ b/.gsd.migrating/event-log.jsonl @@ -0,0 +1 @@ +{"v":2,"cmd":"plan-milestone","params":{"milestoneId":"M001"},"ts":"2026-05-13T05:31:30.650Z","actor":"agent","actor_name":"executor-01","trigger_reason":"plan-phase complete","hash":"e7646f64e62daa33","session_id":"dbe22dc5-e220-475e-aa1b-14d620ab6d6d"} diff --git a/.gsd.migrating/milestones/M001/M001-CONTEXT.md b/.gsd.migrating/milestones/M001/M001-CONTEXT.md new file mode 100644 index 0000000..25b6482 --- /dev/null +++ b/.gsd.migrating/milestones/M001/M001-CONTEXT.md @@ -0,0 +1,169 @@ +# M001: TCP Transport + +**Gathered:** 2026-01-13 +**Status:** Ready for planning + +## Project Description + +Add TCP socket transport to wx-cli's daemon communication layer, enabling remote clients to query WeChat data over the network. Refactor the existing platform-specific IPC code into a trait-based abstraction to eliminate duplication and make future transport additions easy. + +## Why This Milestone + +Currently wx-cli only supports local IPC (Unix sockets on macOS/Linux, named pipes on Windows). This limits usage to the same machine as the WeChat daemon. Adding TCP transport enables remote access, containerized deployments, and multi-machine setups. + +## User-Visible Outcome + +### When this milestone is complete, the user can: + +- Start the daemon with TCP listening: `wx daemon start --tcp 127.0.0.1:9876` +- Query WeChat data over TCP: `wx sessions --tcp 127.0.0.1:9876` +- Use all existing commands without `--tcp` and get unchanged local behavior +- Check daemon status and logs over TCP: `wx daemon status --tcp 127.0.0.1:9876` + +### Entry point / environment + +- Entry point: `wx` CLI command with global `--tcp host:port` flag +- Environment: local dev or remote machine (TCP network) +- Live dependencies involved: wx-daemon process + +## Completion Class + +- Contract complete means: Transport traits defined, all three implementations compile, protocol handling is shared +- Integration complete means: Daemon listens on local + TCP simultaneously, client connects via TCP and gets correct response +- Operational complete means: Daemon starts with `--tcp`, handles bind errors cleanly, client fails with clear error when TCP unreachable + +## Final Integrated Acceptance + +To call this milestone complete, we must prove: + +- `cargo check` passes on macOS, Linux, and Windows targets +- Daemon started with `--tcp 127.0.0.1:9876` accepts TCP connections and responds correctly +- Client with `--tcp 127.0.0.1:9876` returns same results as local transport +- Client with `--tcp 127.0.0.1:9999` (unreachable) fails with clear error within 15s +- Commands without `--tcp` still work via local transport (no regression) + +## Architectural Decisions + +### Transport abstraction via traits + +**Decision:** Use `Listener` and `Connector` traits to abstract transport primitives, implement for Unix socket, Windows named pipe, and TCP. + +**Rationale:** Current code has ~50 lines of duplicated JSON-line protocol handling across Unix/Windows. Traits eliminate duplication and provide clear extension point for future transports (TLS, WebSocket). + +**Alternatives Considered:** +- Continue #[cfg] branching — current approach, hard to extend, duplicative +- `interprocess` crate for all transports — doesn't support TCP natively +- Abstract at protocol level only — would still need per-platform listener/connection code + +### One request per connection (unchanged) + +**Decision:** Keep existing protocol model — one JSON-line request per connection, no keepalive or pooling. + +**Rationale:** Matches existing behavior, minimal complexity, sufficient for CLI usage patterns. + +**Alternatives Considered:** +- Persistent connections with multiplexing — adds protocol complexity, not needed for CLI +- Connection pooling — overkill for single-client CLI tool + +### Global CLI flag for TCP + +**Decision:** `--tcp host:port` as global clap flag on root `Cli` struct, inherited by all subcommands. + +**Rationale:** Discoverable, consistent UX. User specifies once, affects all commands. + +**Alternatives Considered:** +- Environment variables — hidden, harder to discover +- Per-subcommand flag — repetitive, inconsistent +- Config file only — requires edit before use + +### No built-in TCP security + +**Decision:** No TLS, no auth tokens, no IP whitelist in this milestone. Bind exactly as user specifies. + +**Rationale:** User handles firewall/ACL at OS level. Adding TLS requires cert management, tokio-rustls dependency, and significantly more complexity. Can be added later non-breaking. + +**Alternatives Considered:** +- Default to localhost-only — too restrictive, user should control bind address +- Built-in IP whitelist — adds config complexity, OS firewall is better tool + +## Error Handling Strategy + +- **TCP bind failure:** `"TCP bind failed on {addr}: {reason}"` — daemon aborts startup +- **TCP connection failure:** `"Failed to connect to {addr}: {reason}"` — hard error, no fallback +- **Connection timeout:** 15s connect, 120s read/write (matches existing) +- **Connection dropped mid-request:** `"Connection lost: daemon closed or network error"` +- **Mixed transport mismatch:** `"No daemon listening on {addr}"` — same as current "daemon not alive" path +- **No `--tcp`:** Existing local transport behavior, no change + +## Risks and Unknowns + +- Windows named pipe refactoring may require `interprocess` crate changes — the crate's API differs from std Unix sockets +- `daemon start` subcommand needs to handle existing auto-start behavior (currently daemon starts on first query via `ensure_daemon()`) + +## Existing Codebase / Prior Art + +- `src/daemon/server.rs` — current IPC server, needs refactoring to use Listener trait +- `src/cli/transport.rs` — current IPC client, needs refactoring to use Connector trait +- `src/ipc.rs` — protocol types (Request/Response), well-abstracted, no changes needed +- `src/config.rs` — needs tcp_addr field extension + +## Relevant Requirements + +- R001 — TCP transport on server (M001/S01) +- R002 — TCP transport on client (M001/S02) +- R003 — Transport abstraction layer (M001/S01) +- R004 — Global `--tcp` CLI flag (M001/S02) +- R005 — Daemon start command (M001/S01) +- R006 — Cross-platform compilation (M001/S01) +- R007 — Error handling for TCP failures (M001/S02) +- R008 — Integration: CLI ↔ daemon over TCP (M001/S04) + +## Scope + +### In Scope + +- Trait-based transport abstraction (Listener, Connector) +- TCP implementation (TcpListener, TcpStream) +- Global `--tcp host:port` CLI flag +- `wx daemon start` subcommand +- Error handling for TCP failures +- Cross-platform compilation + +### Out of Scope / Non-Goals + +- TLS encryption +- Authentication tokens +- IP whitelisting +- Connection pooling / keepalive +- Changing the JSON-line protocol + +## Technical Constraints + +- Must maintain backwards compatibility: no `--tcp` = existing behavior +- tokio is already a dependency (TcpListener/TcpStream available) +- `interprocess` crate for Windows named pipes — API differs from std + +## Integration Points + +- `src/daemon/server.rs` → `src/transport/` — server uses Listener trait +- `src/cli/transport.rs` → `src/transport/` — client uses Connector trait +- `src/config.rs` → optional tcp_addr field +- `src/cli/mod.rs` → global --tcp flag on Cli struct + +## Testing Requirements + +- `cargo check` on x86_64-unknown-linux-gnu, x86_64-pc-windows-msvc, and current platform +- Unit tests for transport::protocol.rs (JSON round-trip) +- Existing scanner tests continue passing +- Manual smoke test: daemon on TCP, client queries over TCP + +## Acceptance Criteria + +- S01: Transport traits defined, all implementations compile on all platforms, existing behavior unchanged +- S02: `wx daemon start --tcp 127.0.0.1:9876` starts daemon listening on TCP +- S03: `wx sessions --tcp 127.0.0.1:9876` connects via TCP and returns correct results +- S04: End-to-end TCP communication verified manually on localhost + +## Open Questions + +- None — scope confirmed, architecture agreed, error strategy defined \ No newline at end of file diff --git a/.gsd.migrating/milestones/M001/M001-ROADMAP.md b/.gsd.migrating/milestones/M001/M001-ROADMAP.md new file mode 100644 index 0000000..18f62c2 --- /dev/null +++ b/.gsd.migrating/milestones/M001/M001-ROADMAP.md @@ -0,0 +1,21 @@ +# M001: TCP Transport + +**Vision:** Add TCP socket transport to wx-cli's daemon communication layer with trait-based abstraction, enabling remote clients to query WeChat data over the network. + +## Slices + +- [ ] **S01: Transport abstraction layer** `risk:high` `depends:[]` + > After this: Refactor complete, `cargo check` passes on all platforms, existing behavior unchanged. Transport traits defined and implemented for Unix socket + Windows named pipe. + +- [ ] **S02: TCP server support** `risk:medium` `depends:[S01]` + > After this: `wx daemon start --tcp 127.0.0.1:9876` starts daemon listening on TCP port 9876 + +- [ ] **S03: TCP client + global --tcp flag** `risk:medium` `depends:[S01]` + > After this: `wx sessions --tcp 127.0.0.1:9876` connects via TCP and returns session data + +- [ ] **S04: Integration smoke test** `risk:low` `depends:[S02,S03]` + > After this: Daemon on TCP + client queries return same data as local transport + +## Boundary Map + +Not provided. diff --git a/.gsd.migrating/notifications.jsonl b/.gsd.migrating/notifications.jsonl new file mode 100644 index 0000000..7e85ce0 --- /dev/null +++ b/.gsd.migrating/notifications.jsonl @@ -0,0 +1,31 @@ +{"id":"ba5f368a-35d5-4624-bf93-53fd53117126","ts":"2026-05-13T04:37:51.107Z","severity":"info","message":"Use /gsd prefs project to update project preferences.","source":"notify","read":false} +{"id":"826493b3-d1b2-481a-8989-db9d9c4217b7","ts":"2026-05-13T04:37:57.391Z","severity":"info","message":"Using existing global GSD skill preferences at C:\\Users\\david\\.gsd\\PREFERENCES.md","source":"notify","read":false} +{"id":"25bd9781-bb82-4209-bb02-1a8b8b4d710a","ts":"2026-05-13T04:37:57.395Z","severity":"info","message":"GSD preferences (global) — pick a category to configure.","source":"notify","read":false} +{"id":"bedf9f7f-38b0-4724-abd0-2f044eb0a7d2","ts":"2026-05-13T04:40:53.586Z","severity":"info","message":"Saved global preferences to C:\\Users\\david\\.gsd\\PREFERENCES.md","source":"notify","read":false} +{"id":"2c83aa1f-d2a4-4faf-903e-9bc1b361a665","ts":"2026-05-13T04:41:31.640Z","severity":"warning","message":"[migration] Provider-specific default fallback used without an explicit available model; configure provider-aware model preferences before removing defaults.","source":"workflow-logger","read":false} +{"id":"07df36ac-fb0f-4895-a3d9-845e563b47f3","ts":"2026-05-13T05:02:59.382Z","severity":"info","message":"Using existing global GSD skill preferences at C:\\Users\\david\\.gsd\\PREFERENCES.md","source":"notify","read":false} +{"id":"0bf8cebb-68d5-4d14-9c6a-6cdda12b33e2","ts":"2026-05-13T05:02:59.385Z","severity":"info","message":"GSD preferences (global) — pick a category to configure.","source":"notify","read":false} +{"id":"b91acb14-a891-4934-a0e2-4bec6e0ed178","ts":"2026-05-13T05:03:22.769Z","severity":"info","message":"Mode: solo — defaults: auto_push=true, push_branches=false, pre_merge_check=auto, merge_strategy=squash, isolation=worktree, unique_milestone_ids=false","source":"notify","read":false} +{"id":"24385a47-fae4-433a-83c8-238dd1ee450d","ts":"2026-05-13T05:05:30.603Z","severity":"info","message":"Saved global preferences to C:\\Users\\david\\.gsd\\PREFERENCES.md","source":"notify","read":false} +{"id":"52a852d3-aada-423a-9c23-0802a0735162","ts":"2026-05-13T05:05:41.228Z","severity":"info","message":"GSD — Get Shit Done\n\nQUICK START\n /gsd start Start a workflow template\n /gsd Run next unit (same as /gsd next)\n /gsd auto Run all queued units continuously\n /gsd pause Pause auto-mode\n /gsd stop Stop auto-mode gracefully\n\nVISIBILITY\n /gsd status Dashboard (Ctrl+Alt+G / Ctrl+Shift+G)\n /gsd parallel watch Parallel monitor (Ctrl+Alt+P)\n /gsd notifications Notification history (Ctrl+Alt+N / Ctrl+Shift+N)\n /gsd visualize Intera…","source":"notify","read":false} +{"id":"3d539aca-9626-4add-9792-45a220fde9ae","ts":"2026-05-13T05:05:44.707Z","severity":"info","message":"Project detected:\n rust project\n Project files: Cargo.toml, .github/workflows\n CI/CD: detected\n Verification: cargo test, cargo clippy","source":"notify","read":false} +{"id":"1c077a14-1cba-4370-9e08-d3d4842b766e","ts":"2026-05-13T05:06:30.925Z","severity":"info","message":"Installing Rust, Skill Authoring, Document Handling skills...","source":"notify","read":false} +{"id":"9da47155-1524-4474-8692-48e3e15ba878","ts":"2026-05-13T05:06:30.935Z","severity":"info","message":"Installing Rust Async Patterns, CI/CD Automation, Code Review & Quality, Git Advanced Workflows skills...","source":"notify","read":false} +{"id":"dbdce8ef-473c-4855-b3ce-42b1040e74a3","ts":"2026-05-13T05:06:30.943Z","severity":"info","message":"Installing Skill Discovery skills...","source":"notify","read":false} +{"id":"0d1ff703-1a0f-432b-9fac-4033d5069a2f","ts":"2026-05-13T05:06:30.950Z","severity":"info","message":"Failed to install Rust — try manually: npx skills add anthropics/skills","source":"notify","read":false} +{"id":"8faf17c7-fa9f-4088-be28-583abc163801","ts":"2026-05-13T05:06:30.951Z","severity":"info","message":"Failed to install Rust Async Patterns — try manually: npx skills add wshobson/agents","source":"notify","read":false} +{"id":"66f7d6e4-4ef7-46ab-901e-0de1faa4890c","ts":"2026-05-13T05:06:30.952Z","severity":"info","message":"Failed to install CI/CD Automation — try manually: npx skills add wshobson/agents","source":"notify","read":false} +{"id":"3380ca5b-612f-48f4-8355-8c9096a0e43f","ts":"2026-05-13T05:06:30.952Z","severity":"info","message":"Failed to install Skill Discovery — try manually: npx skills add vercel-labs/skills","source":"notify","read":false} +{"id":"99a403d9-4270-4dc7-a250-8a938736f717","ts":"2026-05-13T05:06:30.953Z","severity":"info","message":"Failed to install Skill Authoring — try manually: npx skills add anthropics/skills","source":"notify","read":false} +{"id":"2939bc3a-0d64-431e-ae46-3823d62df1c0","ts":"2026-05-13T05:06:30.954Z","severity":"info","message":"Failed to install Document Handling — try manually: npx skills add anthropics/skills","source":"notify","read":false} +{"id":"2f34be20-36b9-4d3f-815a-14f18e148396","ts":"2026-05-13T05:06:30.954Z","severity":"info","message":"Failed to install Code Review & Quality — try manually: npx skills add wshobson/agents","source":"notify","read":false} +{"id":"f983e68f-0ee7-4af4-9e4a-04222c6f27e5","ts":"2026-05-13T05:06:30.955Z","severity":"info","message":"Failed to install Git Advanced Workflows — try manually: npx skills add wshobson/agents","source":"notify","read":false} +{"id":"f246d948-6cff-405c-b6dd-a88d72c014ef","ts":"2026-05-13T05:06:43.256Z","severity":"info","message":"Codebase map generated: 52 files","source":"notify","read":false} +{"id":"a6ccb4fe-fc00-4162-bbd3-2e8a1a8684c4","ts":"2026-05-13T05:06:43.514Z","severity":"info","message":"GSD initialized. Starting your first milestone...","source":"notify","read":false} +{"id":"29a93a25-8daa-44a1-8eb5-4a4e9b01e59d","ts":"2026-05-13T05:06:45.030Z","severity":"info","message":"Analyzing codebase...","source":"notify","read":false} +{"id":"6a93a35a-02cf-4b7e-8367-79fa01bfed6e","ts":"2026-05-13T05:06:45.043Z","severity":"success","message":"✓ Analyzed codebase","source":"notify","read":false} +{"id":"853a2d66-548f-4b18-84fe-71c74b8741b5","ts":"2026-05-13T05:06:45.044Z","severity":"info","message":"Reviewing prior context...","source":"notify","read":false} +{"id":"39321b35-1395-4a4b-8f04-63a8d9d8129b","ts":"2026-05-13T05:06:45.053Z","severity":"success","message":"✓ Reviewed prior context","source":"notify","read":false} +{"id":"0fa5db21-6a50-4fec-9ddd-c0020c8c35f4","ts":"2026-05-13T05:23:12.475Z","severity":"info","message":"discuss-milestone M001 is waiting for your approval - pausing before more tool calls run.","source":"notify","read":false} +{"id":"24d68af1-955c-4fbd-a841-357605a9c2f0","ts":"2026-05-13T05:25:36.823Z","severity":"info","message":"discuss-milestone M001 is waiting for your approval - pausing before more tool calls run.","source":"notify","read":false} +{"id":"b8ff6fb1-2775-4790-a494-2c0da94f8ee1","ts":"2026-05-13T05:32:05.786Z","severity":"success","message":"Milestone M001 ready.","source":"notify","read":false} diff --git a/.gsd.migrating/state-manifest.json b/.gsd.migrating/state-manifest.json new file mode 100644 index 0000000..11014df --- /dev/null +++ b/.gsd.migrating/state-manifest.json @@ -0,0 +1,160 @@ +{ + "version": 1, + "exported_at": "2026-05-13T05:31:30.649Z", + "milestones": [ + { + "id": "M001", + "title": "TCP Transport", + "status": "active", + "depends_on": [], + "created_at": "2026-05-13T05:31:30.629Z", + "completed_at": null, + "vision": "Add TCP socket transport to wx-cli's daemon communication layer with trait-based abstraction, enabling remote clients to query WeChat data over the network.", + "success_criteria": [], + "key_risks": [ + { + "risk": "Windows named pipe refactoring may require interprocess crate API changes", + "whyItMatters": "interprocess crate API differs from std Unix sockets, may need adaptation for trait compatibility" + }, + { + "risk": "Daemon start subcommand conflicts with existing auto-start behavior", + "whyItMatters": "Currently daemon auto-starts on first query via ensure_daemon(). New explicit start must coexist." + } + ], + "proof_strategy": [ + { + "riskOrUnknown": "Windows named pipe trait compatibility with interprocess crate", + "retireIn": "S01", + "whatWillBeProven": "Transport traits work for Unix socket and named pipe" + }, + { + "riskOrUnknown": "TCP bind and accept behavior on all platforms", + "retireIn": "S02", + "whatWillBeProven": "TCP listener accepts connections and handles requests correctly" + }, + { + "riskOrUnknown": "Protocol works identically over TCP as over local transport", + "retireIn": "S04", + "whatWillBeProven": "End-to-end TCP communication returns correct results" + } + ], + "verification_contract": "- Contract verification: `cargo check` on all three targets, unit tests for protocol handling\n- Integration verification: Manual smoke test of CLI ↔ daemon over TCP on localhost\n- Operational verification: Daemon starts/stops cleanly with TCP, handles bind errors\n- UAT / human verification: Verify TCP results match local transport results", + "verification_integration": "Manual smoke test: daemon on TCP + client queries return same data as local transport", + "verification_operational": "Daemon starts with --tcp, binds to specified address, handles errors cleanly. Client connects via TCP, fails clearly on unreachable address.", + "verification_uat": "", + "definition_of_done": [ + "All slice deliverables complete", + "Transport abstraction layer wired into daemon and client", + "cargo check passes on macOS, Linux, and Windows targets", + "Daemon can listen on local + TCP simultaneously", + "Client connects via TCP with --tcp, errors clearly on failure", + "wx daemon start subcommand works", + "Manual smoke test passes: CLI ↔ daemon over TCP on localhost", + "No regression to local-only transport" + ], + "requirement_coverage": "Covers: R001, R002, R003, R004, R005, R006, R007, R008\nPartially covers: none\nLeaves for later: R020 (TLS), R021 (Auth), R022 (Keepalive)\nOrphan risks: none", + "boundary_map_markdown": "Not provided.", + "sequence": 0 + } + ], + "slices": [ + { + "milestone_id": "M001", + "id": "S01", + "title": "Transport abstraction layer", + "status": "pending", + "risk": "high", + "depends": [], + "demo": "Refactor complete, `cargo check` passes on all platforms, existing behavior unchanged. Transport traits defined and implemented for Unix socket + Windows named pipe.", + "created_at": "2026-05-13T05:31:30.630Z", + "completed_at": null, + "full_summary_md": "", + "full_uat_md": "", + "goal": "Refactor transport layer into trait-based abstraction, eliminating platform-specific duplication. Implement Unix socket and Windows named pipe using new traits. Add `wx daemon start` subcommand.", + "success_criteria": "- `cargo check` passes on all three target platforms\n- Existing CLI commands work unchanged (no regression)\n- Transport traits (Listener, Connector) defined in `src/transport/traits.rs`\n- Protocol handling shared in `src/transport/protocol.rs`\n- `wx daemon start` subcommand exists and starts daemon", + "proof_level": "contract", + "integration_closure": "Daemon starts via `wx daemon start` and listens on local transport (Unix socket / named pipe). Client queries work via local transport as before.", + "observability_impact": "Daemon startup logs show which transports are active", + "sequence": 1, + "replan_triggered_at": null, + "is_sketch": 0, + "sketch_scope": "" + }, + { + "milestone_id": "M001", + "id": "S02", + "title": "TCP server support", + "status": "pending", + "risk": "medium", + "depends": [ + "S01" + ], + "demo": "`wx daemon start --tcp 127.0.0.1:9876` starts daemon listening on TCP port 9876", + "created_at": "2026-05-13T05:31:30.630Z", + "completed_at": null, + "full_summary_md": "", + "full_uat_md": "", + "goal": "Implement TCP server support. Add `--tcp` flag to daemon start. Daemon listens on local transport AND TCP simultaneously when --tcp is specified.", + "success_criteria": "- `wx daemon start --tcp 127.0.0.1:9876` starts daemon on both local and TCP\n- TCP connections accepted and handled correctly\n- TCP bind failure produces clear error message\n- `cargo check` passes on all platforms", + "proof_level": "contract", + "integration_closure": "Daemon accepts TCP connections. JSON-line protocol works over TCP. Bind errors are clear.", + "observability_impact": "Daemon logs show TCP bind address and accepted connections", + "sequence": 2, + "replan_triggered_at": null, + "is_sketch": 0, + "sketch_scope": "" + }, + { + "milestone_id": "M001", + "id": "S03", + "title": "TCP client + global --tcp flag", + "status": "pending", + "risk": "medium", + "depends": [ + "S01" + ], + "demo": "`wx sessions --tcp 127.0.0.1:9876` connects via TCP and returns session data", + "created_at": "2026-05-13T05:31:30.630Z", + "completed_at": null, + "full_summary_md": "", + "full_uat_md": "", + "goal": "Implement TCP client support. Add global `--tcp host:port` flag to CLI. Client connects directly via TCP when flag is specified, with no local fallback.", + "success_criteria": "- `wx sessions --tcp 127.0.0.1:9876` connects via TCP and returns results\n- `wx sessions --tcp 127.0.0.1:9999` fails with clear error within 15s\n- `wx daemon status --tcp 127.0.0.1:9876` works over TCP\n- `wx daemon logs --tcp 127.0.0.1:9876` works over TCP\n- All commands without --tcp still work via local transport", + "proof_level": "contract", + "integration_closure": "Client connects via TCP to running daemon. All query commands work. Error messages clear on failure.", + "observability_impact": "Client error messages show TCP address when connection fails", + "sequence": 3, + "replan_triggered_at": null, + "is_sketch": 0, + "sketch_scope": "" + }, + { + "milestone_id": "M001", + "id": "S04", + "title": "Integration smoke test", + "status": "pending", + "risk": "low", + "depends": [ + "S02", + "S03" + ], + "demo": "Daemon on TCP + client queries return same data as local transport", + "created_at": "2026-05-13T05:31:30.630Z", + "completed_at": null, + "full_summary_md": "", + "full_uat_md": "", + "goal": "End-to-end integration verification. Daemon on TCP, client queries over TCP, results match local transport.", + "success_criteria": "- Manual smoke test: daemon started with --tcp, client queries over TCP\n- TCP and local transport return identical results\n- No regression to local-only mode\n- All cargo check targets pass", + "proof_level": "integration", + "integration_closure": "Full end-to-end: CLI ↔ daemon over TCP on localhost, same results as local transport.", + "observability_impact": "Observable end-to-end TCP communication between CLI and daemon", + "sequence": 4, + "replan_triggered_at": null, + "is_sketch": 0, + "sketch_scope": "" + } + ], + "tasks": [], + "decisions": [], + "verification_evidence": [] +} \ No newline at end of file diff --git a/notes/ARCH.md b/notes/ARCH.md new file mode 100644 index 0000000..683a2cd --- /dev/null +++ b/notes/ARCH.md @@ -0,0 +1,455 @@ +# wx-cli Architecture Analysis + +## Overview + +**wx-cli** is a cross-platform Rust CLI tool for extracting and querying local WeChat 4.x data. It decrypts SQLCipher-encrypted databases, caches decrypted copies with mtime-aware invalidation, and provides a daemon-based IPC architecture for fast repeated queries. + +**Key characteristics:** +- Single binary, zero runtime dependencies +- Cross-platform: macOS, Linux, Windows +- Millisecond response times via daemon caching +- AI-friendly output (YAML by default, JSON optional) +- All data processed locally, no network calls + +--- + +## High-Level Architecture + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ wx (CLI client) │ +│ src/cli/mod.rs - clap-based command parsing │ +│ Commands: init, sessions, history, search, contacts, export, │ +│ unread, members, new-messages, stats, favorites, sns-* │ +└────────────────────────────┬────────────────────────────────────────┘ + │ IPC (Unix socket / Windows named pipe) + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ wx-daemon (background process) │ +│ src/daemon/mod.rs - tokio async runtime │ +│ │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ │ +│ │ DbCache │ │ Names │ │ IPC Server │ │ +│ │ (mtime-aware)│ │ (contact map)│ │ (JSON line protocol) │ │ +│ │ src/daemon/ │ │ src/daemon/ │ │ src/daemon/ │ │ +│ │ cache.rs │ │ query.rs │ │ server.rs │ │ +│ └──────────────┘ └──────────────┘ └──────────────────────┘ │ +│ │ +│ On startup: │ +│ 1. Load config + keys from ~/.wx-cli/ │ +│ 2. Pre-warm: decrypt session.db, sns.db, load contacts │ +│ 3. Listen on socket/pipe for requests │ +└────────────────────────────┬────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ Crypto Layer │ +│ src/crypto/mod.rs + wal.rs │ +│ │ +│ - SQLCipher 4 page decryption (AES-256-CBC) │ +│ - WAL (Write-Ahead Log) application │ +│ - Streaming decryption (page-by-page, avoids full-file load) │ +└────────────────────────────┬────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ Scanner Layer │ +│ src/scanner/{macos,linux,windows}.rs │ +│ │ +│ Platform-specific memory scanners: │ +│ - macOS: Mach VM API (task_for_pid, mach_vm_region, mach_vm_read) │ +│ - Linux: /proc//mem + /proc//maps │ +│ - Windows: CreateToolhelp32Snapshot + ReadProcessMemory │ +│ │ +│ Pattern: x'<64hex_key><32hex_salt>' in WeChat process memory │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Module Breakdown + +### 1. Entry Point (`src/main.rs`) + +```rust +fn main() { + if std::env::var("WX_DAEMON_MODE").is_ok() { + daemon::run(); // Background daemon mode + } else { + cli::run(); // CLI client mode + } +} +``` + +Single binary acts as both client and daemon. Daemon spawned via `WX_DAEMON_MODE=1` env var. + +--- + +### 2. CLI Layer (`src/cli/`) + +**`mod.rs`** - Command definitions via clap derive macros: +- 17 subcommands (Init, Sessions, History, Search, Contacts, Export, Unread, Members, NewMessages, Stats, Favorites, SnsNotifications, SnsFeed, SnsSearch, Daemon) +- Each command dispatches to dedicated module (e.g., `history::cmd_history`) +- All commands share `--json` flag for output format toggle + +**`transport.rs`** - IPC client: +- `ensure_daemon()` - auto-start daemon if not running +- `send()` - JSON line protocol over Unix socket / Windows named pipe +- Timeout handling (15s startup, 120s request) +- Permission preflight check for ~/.wx-cli/ directory + +**Command modules** (`sessions.rs`, `history.rs`, etc.): +- Parse CLI args → build IPC `Request` +- Send to daemon → receive `Response` +- Format output (YAML/JSON) via `output.rs` + +--- + +### 3. Daemon Layer (`src/daemon/`) + +**`mod.rs`** - Daemon lifecycle: +```rust +async fn async_run() -> Result<()> { + // 1. Create ~/.wx-cli/ + cache/ directories + // 2. Write PID file + // 3. Setup signal handlers (SIGTERM/SIGINT) + // 4. Load config + keys + // 5. Initialize DbCache (mtime-aware decryption cache) + // 6. Pre-warm: load contacts, decrypt session.db + sns.db + // 7. Start IPC server (blocking loop) +} +``` + +**`cache.rs`** - DbCache (critical performance component): +- `HashMap` in-memory cache +- `CacheEntry`: `{ db_mtime, wal_mtime, decrypted_path }` +- **mtime-aware invalidation**: re-decrypt only when `.db` or `.db-wal` mtime changes +- Persistent mtime records in `~/.wx-cli/cache/_mtimes.json` +- Cache reuse on daemon restart (avoids re-decryption) +- Uses MD5 hash of rel_key for cache filename + +**`server.rs`** - IPC server: +- Unix: `tokio::net::UnixListener` on `~/.wx-cli/daemon.sock` +- Windows: `interprocess` named pipe `\\.\pipe\wx-cli-daemon` +- One connection per request, JSON line protocol +- `dispatch()` routes `Request` → query functions + +**`query.rs`** - Query implementations (~1500 lines): +- `Names` struct: contact name cache + MD5→username lookup + verify_flags +- `chat_type_of()`: classify as `private`/`group`/`official_account`/`folded` +- Query functions: `q_sessions`, `q_history`, `q_search`, `q_contacts`, `q_unread`, `q_members`, `q_new_messages`, `q_stats`, `q_favorites`, `q_sns_*` +- Message parsing: zstd decompression, XML extraction (appmsg, sysmsg, revokemsg) +- Uses `spawn_blocking` for SQLite queries (rusqlite is sync) + +--- + +### 4. Crypto Layer (`src/crypto/`) + +**`mod.rs`** - SQLCipher 4 decryption: +```rust +// Constants +PAGE_SZ = 4096 +SALT_SZ = 16 +RESERVE_SZ = 80 // IV(16) + HMAC(64) + +// Key operations +fn decrypt_page(enc_key: &[u8; 32], page_data: &[u8], pgno: u32) -> Vec +fn full_decrypt(db_path: &Path, out_path: &Path, enc_key: &[u8; 32]) +``` + +**Algorithm:** +- AES-256-CBC decryption +- IV located at page end: `PAGE_SZ - RESERVE_SZ` offset +- Page 1 special handling: skip 16-byte SALT, write SQLite magic header +- Other pages: decrypt `[0..PAGE_SZ-RESERVE_SZ]` +- Streaming (page-by-page) to avoid full-file memory load + +**`wal.rs`** - WAL application: +- WAL header: 32 bytes (magic, format, page_sz, ckpt_seq, salt1/2, cksum1/2) +- Frame: 24-byte header + PAGE_SZ data +- Frame matching via salt1/2 validation +- Random-write to decrypted DB at `(pgno-1) * PAGE_SZ` + +--- + +### 5. Scanner Layer (`src/scanner/`) + +**Common interface** (`mod.rs`): +```rust +pub struct KeyEntry { + db_name: String, // relative path + enc_key: String, // 64-char hex (32 bytes) + salt: String, // 32-char hex (16 bytes) +} + +pub fn scan_keys(db_dir: &Path) -> Result> // platform-specific +pub fn read_db_salt(path: &Path) -> Option +pub fn collect_db_salts(db_dir: &Path) -> Vec<(String, String)> +``` + +**Pattern searched**: `x'<96 hex chars>'` = 64-char key + 32-char salt + +**macOS** (`macos.rs`): +- `task_for_pid` → get Mach task port (requires root + ad-hoc signed WeChat) +- `mach_vm_region` → enumerate VM regions +- `mach_vm_read` → read 2MB chunks +- Filter: `VM_PROT_READ | VM_PROT_WRITE` regions only +- Deduplication by (key, salt) pair + +**Linux** (`linux.rs`): +- `/proc//comm` → find `wechat`/`weixin` process +- `/proc//maps` → parse `rw-` regions +- `/proc//mem` → seek + read +- Same chunk/dedup strategy + +**Windows** (`windows.rs`): +- `CreateToolhelp32Snapshot` → find `Weixin.exe` +- `OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION)` +- `VirtualQueryEx` → enumerate `MEM_COMMIT + PAGE_READWRITE` regions +- `ReadProcessMemory` → chunk read + +--- + +### 6. IPC Protocol (`src/ipc.rs`) + +**Request** (tagged enum): +```rust +pub enum Request { + Ping, + Sessions { limit: usize }, + History { chat, limit, offset, since, until, msg_type }, + Search { keyword, chats, limit, since, until, msg_type }, + Contacts { query, limit }, + Unread { limit, filter }, + Members { chat }, + NewMessages { state, limit }, + Stats { chat, since, until }, + Favorites { limit, fav_type, query }, + SnsNotifications { limit, since, until, include_read }, + SnsFeed { limit, since, until, user }, + SnsSearch { keyword, limit, since, until, user }, +} +``` + +**Response**: +```rust +pub struct Response { + ok: bool, + error: Option, + data: Value, // flattened JSON +} +``` + +Protocol: newline-delimited JSON, one request per connection. + +--- + +### 7. Config Layer (`src/config.rs`) + +**Config struct**: +```rust +pub struct Config { + db_dir: PathBuf, // WeChat db_storage path + keys_file: PathBuf, // all_keys.json + decrypted_dir: PathBuf, // (unused, cache dir used instead) + wechat_process: String, // process name for scanner +} +``` + +**Paths**: +- `cli_dir()`: `~/.wx-cli/` +- `sock_path()`: `~/.wx-cli/daemon.sock` +- `cache_dir()`: `~/.wx-cli/cache/` +- `mtime_file()`: `~/.wx-cli/cache/_mtimes.json` + +**Auto-detection** (`auto_detect_db_dir()`): +- macOS: `~/Library/Containers/com.tencent.xinWeChat/.../xwechat_files/*/db_storage` +- Linux: `~/Documents/xwechat_files/*/db_storage` + legacy path +- Windows: `%APPDATA%/Tencent/xwechat/config/*.ini` → parse data root + +--- + +## Data Flow + +### Init Flow (`wx init`) + +``` +1. Auto-detect db_dir → scan for db_storage directory +2. collect_db_salts(db_dir) → (salt_hex, rel_path) list +3. scan_keys(db_dir) → memory scan → (key_hex, salt_hex) candidates +4. Match: salt_hex == db_salt → KeyEntry { db_name, enc_key, salt } +5. Write ~/.wx-cli/config.json + ~/.wx-cli/all_keys.json +``` + +### Query Flow (e.g., `wx history "张三"`) + +``` +1. CLI: parse args → Request::History { chat: "张三", limit: 50 } +2. transport::ensure_daemon() → start if not alive +3. transport::send(Request) → Unix socket/pipe → daemon +4. daemon::dispatch(Request) → q_history() + a. resolve_username("张三") → "wxid_xxx" (fuzzy match against Names) + b. find_msg_tables(db, names, username) → [(db_path, "Msg_")] + c. spawn_blocking: SQLite query on decrypted db_path + d. decompress_message (zstd) + fmt_content (XML parsing) +5. Response::ok(json!{ chat, messages, ... }) +6. CLI: output.rs → YAML/JSON formatting +``` + +### Decryption Flow (DbCache::get) + +``` +1. Check in-memory cache: if entry.mtime matches → return cached path +2. mtime mismatch or missing → spawn_blocking decrypt: + a. crypto::full_decrypt(db_path, out_path, enc_key) + b. If .db-wal exists: wal::apply_wal(wal_path, out_path, enc_key) +3. Update cache entry + persist mtimes to _mtimes.json +4. Return decrypted path for SQLite query +``` + +--- + +## Database Schema Knowledge + +**session/session.db**: +- `SessionTable`: username, unread_count, summary, last_timestamp, last_msg_type, last_msg_sender + +**contact/contact.db**: +- `contact`: username, nick_name, remark, verify_flag +- `chat_room`: id, owner (for group info) +- `chatroom_member`: room_id, member_id (joined with contact) + +**message/message_N.db**: +- `Msg_`: local_id, local_type, create_time, real_sender_id, message_content, WCDB_CT_message_content +- `Name2Id`: rowid → user_name (sender lookup) +- WCDB_CT = 4 means zstd compression + +**sns/sns.db**: +- `sns_notification`: type (like/comment), from_nickname, content, feed_preview +- `sns_feed_xml`: author, contentDesc, media XML, createTime + +**favorite/favorite.db**: +- `fav_db_item`: local_id, type, update_time, content, fromusr + +--- + +## Performance Optimizations + +1. **mtime-aware caching**: Only re-decrypt when source file changes +2. **Pre-warming**: Decrypt session.db + sns.db + contacts on daemon start +3. **Arc-wrapped Names**: Contact cache shared via Arc, cloned in O(1) +4. **spawn_blocking**: Sync SQLite ops off async runtime +5. **Streaming decrypt**: Page-by-page, no full file in memory +6. **WAL handling**: Apply uncommitted writes without re-decrypt +7. **MD5 table lookup**: `Msg_` → username via precomputed hash map + +--- + +## Security Considerations + +1. **Root/Admin required**: Memory scan needs elevated privileges +2. **No secrets logged**: Keys written to file, never echoed +3. **Socket permissions**: Unix socket mode 0600 +4. **Local-only**: All IPC is localhost, no network exposure +5. **User consent implied**: Only decrypts own WeChat data + +--- + +## Error Handling Patterns + +- `anyhow::Result` throughout +- Context messages for chain debugging +- Graceful degradation: missing tables → fallback paths +- Preflight checks (e.g., ~/.wx-cli writable before daemon spawn) +- Signal handlers for clean shutdown (socket/PID file cleanup) + +--- + +## Cross-Platform Notes + +| Platform | Scanner API | IPC | Privilege | DB Path | +|----------|-------------|-----|-----------|---------| +| macOS | Mach VM | Unix socket | sudo + codesign | ~/Library/Containers/... | +| Linux | /proc/pid/mem | Unix socket | sudo | ~/Documents/xwechat_files | +| Windows | ToolHelp + ReadProcessMemory | Named pipe | Admin | %APPDATA%/Tencent/xwechat | + +--- + +## Testing Coverage + +- `src/crypto/mod.rs`: hex encoding, salt reading, recursive collection +- `src/scanner/macos.rs`: pattern matching (uppercase, dedup, embedded, edge cases) +- Unit tests for helper functions; integration tests would require live WeChat + +--- + +## Extension Points + +1. **New commands**: Add to `cli/mod.rs` enum + dispatch + query.rs function +2. **New message types**: Extend `fmt_type()` + `fmt_content()` parsers +3. **New DB sources**: Add to DbCache key list + query functions +4. **Output formats**: Extend `output.rs` formatter + +--- + +## File Structure Summary + +``` +src/ +├── main.rs # Entry point (daemon/CLI switch) +├── config.rs # Config loading + auto-detect +├── ipc.rs # Request/Response protocol types +├── cli/ +│ ├── mod.rs # clap command definitions + dispatch +│ ├── transport.rs # IPC client + daemon lifecycle +│ ├── output.rs # YAML/JSON formatting +│ ├── init.rs # wx init implementation +│ ├── sessions.rs # etc. (thin wrappers around IPC) +│ └── daemon_cmd.rs # daemon status/stop/logs +├── daemon/ +│ ├── mod.rs # daemon entry + async_run +│ ├── cache.rs # DbCache (mtime-aware decryption cache) +│ ├── server.rs # IPC server (Unix/Windows) +│ └── query.rs # All query implementations +├── crypto/ +│ ├── mod.rs # SQLCipher page decryption +│ └── wal.rs # WAL application +└── scanner/ + ├── mod.rs # common interface + salt collection + ├── macos.rs # Mach VM memory scanner + ├── linux.rs # /proc scanner + └── windows.rs # Windows API scanner +``` + +--- + +## Dependencies + +**Core crates:** +- `clap` (derive) - CLI parsing +- `tokio` (full) - async runtime +- `serde`/`serde_json` - serialization +- `rusqlite` (bundled) - SQLite queries +- `aes`/`cbc`/`hmac`/`sha2`/`pbkdf2` - crypto primitives +- `zstd` - message decompression +- `chrono` - timestamp formatting +- `anyhow` - error handling +- `dirs` - home directory +- `md5` - table name hashing +- `regex` - Msg_ pattern matching + +**Platform-specific:** +- Unix: `libc` (setsid, signal handling) +- Windows: `windows` crate (process/memory APIs), `interprocess` (named pipes) + +--- + +## Summary + +wx-cli is a well-architected Rust project demonstrating: +- Clean separation of CLI/daemon/crypto/scanner layers +- Async-first daemon with sync-offload for SQLite +- Smart caching strategy (mtime-based invalidation) +- Cross-platform memory scanning for SQLCipher key extraction +- AI-friendly output design (YAML default, JSON optional) +- Comprehensive command coverage for WeChat local data \ No newline at end of file diff --git a/notes/TCP.md b/notes/TCP.md new file mode 100644 index 0000000..853d275 --- /dev/null +++ b/notes/TCP.md @@ -0,0 +1,847 @@ +# Communication Layer Analysis & TCP Socket Proposal + +## Current Communication Architecture + +### Layer Overview + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Protocol Layer │ +│ src/ipc.rs │ +│ Request / Response types + JSON serialization │ +│ (Well abstracted - transport-agnostic) │ +└────────────────────────────┬────────────────────────────────┘ + │ +┌────────────────────────────┴────────────────────────────────┐ +│ Server Layer │ +│ src/daemon/server.rs │ +│ Platform-specific listeners + connection handlers │ +│ (POOR abstraction - duplicated logic per platform) │ +└────────────────────────────┬────────────────────────────────┘ + │ +┌────────────────────────────┴────────────────────────────────┐ +│ Client Layer │ +│ src/cli/transport.rs │ +│ Platform-specific connection + send functions │ +│ (POOR abstraction - duplicated logic per platform) │ +└─────────────────────────────────────────────────────────────┘ +``` + +--- + +## Abstraction Assessment + +### Protocol Layer (src/ipc.rs) — **HIGH abstraction** + +**Strengths:** +- Pure data types with serde derive +- No transport-specific code +- Clean API: `Request` enum, `Response` struct +- `to_json_line()` helper for serialization +- Transport-agnostic by design + +**Example:** +```rust +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "cmd", rename_all = "snake_case")] +pub enum Request { + Ping, + Sessions { limit: usize }, + History { chat: String, limit: usize, ... }, + // ... all commands +} + +pub struct Response { + pub ok: bool, + pub error: Option, + #[serde(flatten)] + pub data: Value, +} +``` + +**Verdict:** This layer is well-designed and TCP-ready. No changes needed. + +--- + +### Server Layer (src/daemon/server.rs) — **LOW abstraction** + +**Current structure:** +```rust +// Top-level entry with #[cfg] branching +pub async fn serve(db, names) -> Result<()> { + #[cfg(unix)] + serve_unix(db, names).await?; + #[cfg(windows)] + serve_windows(db, names).await?; +} + +// Unix implementation (40 lines) +#[cfg(unix)] +async fn serve_unix(db, names) -> Result<()> { + let listener = UnixListener::bind(&sock_path)?; + loop { + let (stream, _) = listener.accept().await?; + tokio::spawn(async { handle_connection_unix(stream, db, names) }); + } +} + +#[cfg(unix)] +async fn handle_connection_unix(stream, db, names) -> Result<()> { + let (reader, mut writer) = stream.into_split(); + let mut lines = BufReader::new(reader).lines(); + let line = lines.next_line().await?; + let req: Request = serde_json::from_str(&line)?; + let resp = dispatch(req, &db, &names).await; + writer.write_all(resp.to_json_line()?.as_bytes()).await?; +} + +// Windows implementation (40 lines) - SAME LOGIC, DIFFERENT TYPES +#[cfg(windows)] +async fn serve_windows(db, names) -> Result<()> { + let listener = ListenerOptions::new().name(name).create_tokio()?; + loop { + let conn = listener.accept().await?; + tokio::spawn(async { handle_connection_windows(conn, db, names) }); + } +} + +#[cfg(windows)] +async fn handle_connection_windows(conn, db, names) -> Result<()> { + let (reader, mut writer) = tokio::io::split(conn); + let mut lines = BufReader::new(reader).lines(); + let line = lines.next_line().await?; + let req: Request = serde_json::from_str(&line)?; + let resp = dispatch(req, &db, &names).await; + writer.write_all(resp.to_json_line()?.as_bytes()).await?; +} +``` + +**Problems:** +1. **Duplicated connection handling**: `handle_connection_unix` and `handle_connection_windows` have identical logic +2. **No abstraction for stream types**: `UnixStream` vs `interprocess::Stream` handled separately +3. **No abstraction for listener types**: `UnixListener` vs `interprocess::Listener` handled separately +4. **#[cfg] branching at function level**: Makes extension difficult +5. **`dispatch()` is shared but buried**: Good pattern, but underutilized + +**Duplication count:** ~30 lines of identical JSON-line protocol handling duplicated per platform + +--- + +### Client Layer (src/cli/transport.rs) — **LOW abstraction** + +**Current structure:** +```rust +// is_alive() with #[cfg] branching +pub fn is_alive() -> bool { + #[cfg(unix)] + { + let stream = UnixStream::connect(&sock_path)?; + // ping logic + } + #[cfg(windows)] + { + let stream = Stream::connect(name)?; + // ping logic (different API) + } +} + +// send() with #[cfg] branching +pub fn send(req: Request) -> Result { + ensure_daemon()?; + #[cfg(unix)] + { send_unix(req) } + #[cfg(windows)] + { send_windows(req) } +} + +#[cfg(unix)] +fn send_unix(req: Request) -> Result { + let stream = UnixStream::connect(&sock_path)?; + stream.write_all(serde_json::to_string(&req)? + "\n"); + let line = BufReader::new(&stream).read_line(); + let resp: Response = serde_json::from_str(&line)?; + Ok(resp) +} + +#[cfg(windows)] +fn send_windows(req: Request) -> Result { + let stream = Stream::connect(name)?; + stream.write_all(serde_json::to_string(&req)? + "\n"); + let line = BufReader::new(stream).read_line(); + let resp: Response = serde_json::from_str(&line)?; + Ok(resp) +} +``` + +**Problems:** +1. **Duplicated request/response handling**: Same JSON-line protocol, different stream types +2. **No abstraction for stream type**: Each platform uses different types +3. **`is_alive()` logic differs**: Windows version doesn't do full ping +4. **#[cfg] branching scattered**: 3 separate locations + +**Duplication count:** ~20 lines of identical protocol handling duplicated per platform + +--- + +## Abstraction Score Summary + +| Layer | Abstraction Level | Duplicated Lines | Extension Difficulty | +|----------------|-------------------|------------------|---------------------| +| Protocol | HIGH | 0 | Easy | +| Server | LOW | ~30 | Hard | +| Client | LOW | ~20 | Hard | + +**Total duplicated code:** ~50 lines of identical JSON-line protocol handling + +**Root cause:** No trait abstraction for `Listener` and `Connection` types + +--- + +## Proposed Architecture for TCP Support + +### Strategy: Trait-Based Abstraction + +Introduce traits for transport primitives, implement for: +1. Unix socket (existing) +2. Windows named pipe (existing) +3. TCP socket (new) + +--- + +### New Trait Definitions + +```rust +// src/transport/traits.rs + +use anyhow::Result; +use tokio::io::{AsyncRead, AsyncWrite}; + +/// Trait for accepting connections (server-side) +pub trait Listener: Send + Sync { + type Connection: AsyncRead + AsyncWrite + Send + Sync + 'static; + + async fn accept(&self) -> Result; + fn addr_desc(&self) -> String; // for logging +} + +/// Trait for connecting to server (client-side) +pub trait Connector: Send + Sync { + type Connection: AsyncRead + AsyncWrite + Send + Sync + 'static; + + async fn connect(&self) -> Result; + fn is_available(&self) -> bool; // quick check before connect +} +``` + +--- + +### New Module Structure + +``` +src/transport/ +├── mod.rs # Public API: send(), handle_connection() +├── traits.rs # Listener + Connector traits +├── unix.rs # UnixListener + UnixConnector +├── windows.rs # PipeListener + PipeConnector +├── tcp.rs # TcpListener + TcpConnector +└── protocol.rs # JSON-line protocol handling (shared) +``` + +**Key change:** Protocol handling moves to `protocol.rs`, shared by all transports + +--- + +### Protocol Handler (Shared Code) + +```rust +// src/transport/protocol.rs + +use anyhow::Result; +use tokio::io::{AsyncRead, AsyncWrite, AsyncBufReadExt, AsyncWriteExt, BufReader}; + +use crate::ipc::{Request, Response}; + +/// Handle a single connection (server-side) +pub async fn handle_connection( + conn: C, + db: Arc, + names: Arc>>, +) -> Result<()> { + let (reader, mut writer) = tokio::io::split(conn); + let mut lines = BufReader::new(reader).lines(); + + let line = match lines.next_line().await? { + Some(l) => l, + None => return Ok(()), + }; + + let req: Request = match serde_json::from_str(&line) { + Ok(r) => r, + Err(e) => { + let resp = Response::err(format!("JSON parse error: {}", e)); + writer.write_all(resp.to_json_line()?.as_bytes()).await?; + return Ok(()); + } + }; + + let resp = dispatch(req, db, names).await; + writer.write_all(resp.to_json_line()?.as_bytes()).await?; + Ok(()) +} + +/// Send request and receive response (client-side) +pub async fn send_over_connection( + conn: C, + req: &Request, +) -> Result { + let (reader, mut writer) = tokio::io::split(conn); + + let req_str = serde_json::to_string(req)? + "\n"; + writer.write_all(req_str.as_bytes()).await?; + + let mut lines = BufReader::new(reader).lines(); + let line = lines.next_line().await? + .ok_or_else(|| anyhow::anyhow!("No response received"))?; + + let resp: Response = serde_json::from_str(&line)?; + if !resp.ok { + anyhow::bail!("{}", resp.error.as_deref().unwrap_or("Unknown error")); + } + Ok(resp) +} +``` + +**This eliminates all 50 lines of duplication.** + +--- + +### Unix Socket Implementation + +```rust +// src/transport/unix.rs + +use anyhow::Result; +use tokio::net::{UnixListener, UnixStream}; + +use super::traits::{Listener, Connector}; + +pub struct UnixSocketListener { + listener: UnixListener, + path: std::path::PathBuf, +} + +impl Listener for UnixSocketListener { + type Connection = UnixStream; + + async fn accept(&self) -> Result { + let (stream, _) = self.listener.accept().await?; + Ok(stream) + } + + fn addr_desc(&self) -> String { + self.path.display().to_string() + } +} + +pub struct UnixSocketConnector { + path: std::path::PathBuf, +} + +impl Connector for UnixSocketConnector { + type Connection = UnixStream; + + async fn connect(&self) -> Result { + UnixStream::connect(&self.path).await? + } + + fn is_available(&self) -> bool { + self.path.exists() + } +} + +// Factory functions +pub fn create_listener(path: &std::path::Path) -> Result { + if path.exists() { + std::fs::remove_file(path)?; + } + let listener = UnixListener::bind(path)?; + std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))?; + Ok(UnixSocketListener { listener, path: path.to_owned() }) +} + +pub fn connector(path: &std::path::Path) -> UnixSocketConnector { + UnixSocketConnector { path: path.to_owned() } +} +``` + +--- + +### Windows Named Pipe Implementation + +```rust +// src/transport/windows.rs + +use anyhow::Result; +use interprocess::local_socket::{ + tokio::prelude::*, + GenericNamespaced, ListenerOptions, +}; + +use super::traits::{Listener, Connector}; + +pub struct PipeListener { + listener: interprocess::local_socket::tokio::Listener, + name: String, +} + +impl Listener for PipeListener { + type Connection = interprocess::local_socket::tokio::Stream; + + async fn accept(&self) -> Result { + self.listener.accept().await? + } + + fn addr_desc(&self) -> String { + format!("\\\\.\\pipe\\{}", self.name) + } +} + +pub struct PipeConnector { + name: String, +} + +impl Connector for PipeConnector { + type Connection = interprocess::local_socket::tokio::Stream; + + async fn connect(&self) -> Result { + let ns_name = self.name.to_ns_name::()?; + Stream::connect(ns_name).await? + } + + fn is_available(&self) -> bool { + // Windows named pipes don't have filesystem presence + // Try a quick connect to check + self.connect().await.is_ok() + } +} + +pub fn create_listener(name: &str) -> Result { + let ns_name = name.to_ns_name::()?; + let listener = ListenerOptions::new().name(ns_name).create_tokio()?; + Ok(PipeListener { listener, name: name.to_owned() }) +} + +pub fn connector(name: &str) -> PipeConnector { + PipeConnector { name: name.to_owned() } +} +``` + +--- + +### TCP Socket Implementation (NEW) + +```rust +// src/transport/tcp.rs + +use anyhow::Result; +use tokio::net::{TcpListener, TcpStream}; + +use super::traits::{Listener, Connector}; + +pub struct TcpSocketListener { + listener: TcpListener, + addr: std::net::SocketAddr, +} + +impl Listener for TcpSocketListener { + type Connection = TcpStream; + + async fn accept(&self) -> Result { + let (stream, addr) = self.listener.accept().await?; + eprintln!("[tcp] connection from {}", addr); + Ok(stream) + } + + fn addr_desc(&self) -> String { + self.addr.to_string() + } +} + +pub struct TcpSocketConnector { + addr: std::net::SocketAddr, +} + +impl Connector for TcpSocketConnector { + type Connection = TcpStream; + + async fn connect(&self) -> Result { + TcpStream::connect(&self.addr).await? + } + + fn is_available(&self) -> bool { + // TCP port check - try quick connect + std::net::TcpStream::connect_timeout(&self.addr, std::time::Duration::from_millis(100)).is_ok() + } +} + +pub async fn create_listener(bind: &str) -> Result { + let listener = TcpListener::bind(bind).await?; + let addr = listener.local_addr()?; + Ok(TcpSocketListener { listener, addr }) +} + +pub fn connector(addr: std::net::SocketAddr) -> TcpSocketConnector { + TcpSocketConnector { addr } +} +``` + +--- + +### Server Refactor (src/daemon/server.rs) + +```rust +// src/daemon/server.rs + +use std::sync::Arc; +use crate::transport::{Listener, handle_connection}; + +pub async fn serve( + db: Arc, + names: Arc>>, +) -> Result<()> { + // Determine transport based on config/env + let listeners: Vec> = build_listeners()?; + + for listener in listeners { + eprintln!("[server] listening on {}", listener.addr_desc()); + let db2 = Arc::clone(&db); + let names2 = Arc::clone(&names); + + tokio::spawn(async move { + loop { + match listener.accept().await { + Ok(conn) => { + let db3 = Arc::clone(&db2); + let names3 = Arc::clone(&names2); + tokio::spawn(async move { + if let Err(e) = handle_connection(conn, db3, names3).await { + eprintln!("[server] connection error: {}", e); + } + }); + } + Err(e) => eprintln!("[server] accept error: {}", e), + } + } + }); + } + + // Keep daemon alive + tokio::signal::ctrl_c().await?; + Ok(()) +} + +fn build_listeners() -> Result>> { + let mut listeners = Vec::new(); + + // Always add local transport (Unix/Pipe) + #[cfg(unix)] + listeners.push(Box::new( + crate::transport::unix::create_listener(&crate::config::sock_path())? + )); + + #[cfg(windows)] + listeners.push(Box::new( + crate::transport::windows::create_listener("wx-cli-daemon")? + )); + + // Optionally add TCP (if configured) + if let Ok(tcp_bind) = std::env::var("WX_TCP_BIND") { + let tcp_listener = crate::transport::tcp::create_listener(&tcp_bind).await?; + eprintln!("[server] TCP enabled on {}", tcp_listener.addr_desc()); + listeners.push(Box::new(tcp_listener)); + } + + Ok(listeners) +} +``` + +**Key changes:** +1. Single `serve()` function, no #[cfg] branching +2. `build_listeners()` constructs appropriate transport(s) +3. Can listen on multiple transports simultaneously (local + TCP) +4. `handle_connection()` from `transport::protocol` is shared + +--- + +### Client Refactor (src/cli/transport.rs) + +```rust +// src/cli/transport.rs (renamed to src/transport/mod.rs) + +use anyhow::Result; +use crate::ipc::{Request, Response}; +use crate::transport::{Connector, send_over_connection}; + +pub async fn send(req: Request) -> Result { + ensure_daemon()?; + + // Try connectors in priority order + let connectors = build_connectors(); + + for connector in connectors { + if connector.is_available() { + let conn = connector.connect().await?; + return send_over_connection(conn, &req).await; + } + } + + anyhow::bail!("No available transport to daemon") +} + +fn build_connectors() -> Vec> { + let mut connectors = Vec::new(); + + // Local transport first (faster, more secure) + #[cfg(unix)] + connectors.push(Box::new( + crate::transport::unix::connector(&crate::config::sock_path()) + )); + + #[cfg(windows)] + connectors.push(Box::new( + crate::transport::windows::connector("wx-cli-daemon") + )); + + // TCP fallback (if configured) + if let Ok(tcp_addr) = std::env::var("WX_TCP_ADDR") { + if let Ok(addr) = tcp_addr.parse() { + connectors.push(Box::new( + crate::transport::tcp::connector(addr) + )); + } + } + + connectors +} + +pub fn ensure_daemon() -> Result<()> { + // Try ping on each connector + for connector in build_connectors() { + if connector.is_available() { + // Try quick ping + if let Ok(conn) = connector.connect().await? { + // Use blocking ping for startup check + // ... (existing logic wrapped) + return Ok(()); + } + } + } + + // No daemon found, start it + start_daemon()?; + + // Wait for any connector to become available + let deadline = std::time::Instant::now() + Duration::from_secs(15); + while std::time::Instant::now() < deadline { + for connector in build_connectors() { + if connector.is_available() { + return Ok(()); + } + } + std::thread::sleep(Duration::from_millis(300)); + } + + anyhow::bail!("Daemon startup timeout") +} +``` + +**Key changes:** +1. Async `send()` using `send_over_connection()` +2. `build_connectors()` returns prioritized list +3. Fallback chain: Unix/Pipe → TCP +4. No #[cfg] branching in main logic + +--- + +## Configuration for TCP + +### Environment Variables + +```bash +# Server: enable TCP listener +WX_TCP_BIND=127.0.0.1:9876 # bind address (default: none) +WX_TCP_BIND=0.0.0.0:9876 # allow external connections (security risk) + +# Client: TCP fallback address +WX_TCP_ADDR=127.0.0.1:9876 # connect address +WX_TCP_ADDR=192.168.1.100:9876 # remote daemon +``` + +### Config File Extension + +```json +// ~/.wx-cli/config.json +{ + "db_dir": "...", + "keys_file": "...", + "tcp": { + "bind": "127.0.0.1:9876", // optional + "allow_remote": false // security flag + } +} +``` + +--- + +## Security Considerations for TCP + +### Risks + +1. **No encryption**: JSON-line protocol sent in plaintext +2. **No authentication**: Anyone can query WeChat data +3. **Data exposure**: Chat history, contacts, etc. visible to network + +### Recommended Safeguards + +```rust +// src/transport/tcp.rs + +pub struct TcpSocketListener { + listener: TcpListener, + addr: SocketAddr, + allowed_hosts: Vec, // CIDR whitelist +} + +impl Listener for TcpSocketListener { + async fn accept(&self) -> Result { + let (stream, addr) = self.listener.accept().await?; + + // Check source IP against whitelist + let ip = addr.ip(); + if !self.allowed_hosts.iter().any(|net| net.contains(&ip)) { + eprintln!("[tcp] rejected connection from {}", addr); + return Err(anyhow::anyhow!("IP not in whitelist")); + } + + Ok(stream) + } +} + +// Config +pub struct TcpConfig { + bind: String, + allow_remote: bool, + allowed_hosts: Vec, // ["127.0.0.1/8", "192.168.1.0/24"] +} +``` + +### Authentication Proposal (Optional) + +```rust +// Add to Request enum +pub enum Request { + Auth { token: String }, // new + Ping, + Sessions { ... }, +} + +// Server checks token before processing +async fn dispatch(req: Request, db: &DbCache, names: &Names, auth: &AuthState) -> Response { + if !auth.is_authenticated() && !req.is_auth_request() { + return Response::err("Not authenticated"); + } + // ... normal dispatch +} +``` + +--- + +## Implementation Roadmap + +### Phase 1: Refactor Existing Code + +1. Create `src/transport/` module +2. Define `Listener` and `Connector` traits +3. Move Unix/Pipe implementations to `unix.rs` / `windows.rs` +4. Extract protocol handling to `protocol.rs` +5. Refactor `server.rs` to use trait +6. Refactor `transport.rs` to use trait + +**Effort:** ~4 hours +**Benefit:** Eliminate 50 lines duplication, cleaner architecture + +### Phase 2: Add TCP Support + +1. Create `tcp.rs` with `TcpSocketListener` / `TcpSocketConnector` +2. Update `build_listeners()` / `build_connectors()` +3. Add config parsing for TCP options +4. Add IP whitelist validation + +**Effort:** ~2 hours +**Benefit:** TCP connectivity for remote clients + +### Phase 3: Security Hardening + +1. Add authentication token support +2. TLS wrapper option (tokio-rustls) +3. Connection logging/audit + +**Effort:** ~3 hours +**Benefit:** Production-safe remote access + +--- + +## Backwards Compatibility + +- Local transport (Unix/Pipe) remains default +- TCP opt-in via config/env (not automatic) +- CLI unchanged (same commands) +- Protocol unchanged (same Request/Response types) + +--- + +## Alternative: Zero-Change TCP Proxy + +If refactoring is not desired, a simpler approach: + +```bash +# Use socat/proxy to expose Unix socket over TCP +socat TCP-LISTEN:9876,reuseaddr,fork UNIX-CONNECT:/home/user/.wx-cli/daemon.sock +``` + +**Pros:** No code changes +**Cons:** Requires external tool, no IP filtering, less integrated + +--- + +## Summary + +| Aspect | Current State | Proposed State | +|---------------------------|-------------------------|-----------------------------| +| Protocol abstraction | HIGH (good) | HIGH (unchanged) | +| Transport abstraction | LOW (platform-specific) | HIGH (trait-based) | +| Duplicated code | ~50 lines | 0 lines | +| Extension difficulty | Hard | Easy | +| TCP support | None | Full | +| Multi-listener support | None | Yes (local + TCP) | + +**Recommended path:** Proceed with Phase 1 refactor, then Phase 2 TCP addition. Phase 3 security can follow based on use case. + +--- + +## Code Impact Summary + +| File | Change Type | Lines Changed | +|--------------------------|--------------------|---------------| +| src/transport/mod.rs | New | ~60 | +| src/transport/traits.rs | New | ~20 | +| src/transport/protocol.rs| New (from existing)| ~40 | +| src/transport/unix.rs | New (refactor) | ~40 | +| src/transport/windows.rs | New (refactor) | ~40 | +| src/transport/tcp.rs | New | ~50 | +| src/daemon/server.rs | Refactor | ~30 (from 90) | +| src/cli/transport.rs | Delete (moved) | 0 | +| src/ipc.rs | Unchanged | 0 | + +**Net change:** +250 new lines, -90 old lines, -50 duplication = +110 total +**Complexity reduction:** Platform branching centralized, extension point clear \ No newline at end of file