methodproof 0.7.4__tar.gz → 0.7.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. {methodproof-0.7.4 → methodproof-0.7.6}/CHANGELOG.md +6 -0
  2. {methodproof-0.7.4 → methodproof-0.7.6}/PKG-INFO +35 -10
  3. {methodproof-0.7.4 → methodproof-0.7.6}/README.md +34 -9
  4. {methodproof-0.7.4 → methodproof-0.7.6}/methodproof/__init__.py +1 -1
  5. {methodproof-0.7.4 → methodproof-0.7.6}/methodproof/analysis.py +3 -2
  6. {methodproof-0.7.4 → methodproof-0.7.6}/methodproof/cli.py +41 -1
  7. {methodproof-0.7.4 → methodproof-0.7.6}/methodproof/graph.py +13 -13
  8. {methodproof-0.7.4 → methodproof-0.7.6}/methodproof/migrate_db.py +3 -4
  9. {methodproof-0.7.4 → methodproof-0.7.6}/methodproof/store.py +57 -4
  10. {methodproof-0.7.4 → methodproof-0.7.6}/methodproof/sync.py +7 -1
  11. {methodproof-0.7.4 → methodproof-0.7.6}/pyproject.toml +1 -1
  12. {methodproof-0.7.4 → methodproof-0.7.6}/tests/test_security.py +7 -4
  13. {methodproof-0.7.4 → methodproof-0.7.6}/.github/workflows/ci.yml +0 -0
  14. {methodproof-0.7.4 → methodproof-0.7.6}/.gitignore +0 -0
  15. {methodproof-0.7.4 → methodproof-0.7.6}/LICENSE +0 -0
  16. {methodproof-0.7.4 → methodproof-0.7.6}/methodproof/__main__.py +0 -0
  17. {methodproof-0.7.4 → methodproof-0.7.6}/methodproof/_daemon.py +0 -0
  18. {methodproof-0.7.4 → methodproof-0.7.6}/methodproof/agents/__init__.py +0 -0
  19. {methodproof-0.7.4 → methodproof-0.7.6}/methodproof/agents/base.py +0 -0
  20. {methodproof-0.7.4 → methodproof-0.7.6}/methodproof/agents/music.py +0 -0
  21. {methodproof-0.7.4 → methodproof-0.7.6}/methodproof/agents/terminal.py +0 -0
  22. {methodproof-0.7.4 → methodproof-0.7.6}/methodproof/agents/watcher.py +0 -0
  23. {methodproof-0.7.4 → methodproof-0.7.6}/methodproof/binding.py +0 -0
  24. {methodproof-0.7.4 → methodproof-0.7.6}/methodproof/bip39.py +0 -0
  25. {methodproof-0.7.4 → methodproof-0.7.6}/methodproof/bridge.py +0 -0
  26. {methodproof-0.7.4 → methodproof-0.7.6}/methodproof/config.py +0 -0
  27. {methodproof-0.7.4 → methodproof-0.7.6}/methodproof/crypto.py +0 -0
  28. {methodproof-0.7.4 → methodproof-0.7.6}/methodproof/e2e.py +0 -0
  29. {methodproof-0.7.4 → methodproof-0.7.6}/methodproof/hook.py +0 -0
  30. {methodproof-0.7.4 → methodproof-0.7.6}/methodproof/hooks/__init__.py +0 -0
  31. {methodproof-0.7.4 → methodproof-0.7.6}/methodproof/hooks/claude_code.py +0 -0
  32. {methodproof-0.7.4 → methodproof-0.7.6}/methodproof/hooks/claude_code.sh +0 -0
  33. {methodproof-0.7.4 → methodproof-0.7.6}/methodproof/hooks/cline_hook.sh +0 -0
  34. {methodproof-0.7.4 → methodproof-0.7.6}/methodproof/hooks/codex_hook.sh +0 -0
  35. {methodproof-0.7.4 → methodproof-0.7.6}/methodproof/hooks/gemini_hook.sh +0 -0
  36. {methodproof-0.7.4 → methodproof-0.7.6}/methodproof/hooks/install.py +0 -0
  37. {methodproof-0.7.4 → methodproof-0.7.6}/methodproof/hooks/kiro_hook.sh +0 -0
  38. {methodproof-0.7.4 → methodproof-0.7.6}/methodproof/hooks/mcp_register.py +0 -0
  39. {methodproof-0.7.4 → methodproof-0.7.6}/methodproof/hooks/openclaw/HOOK.md +0 -0
  40. {methodproof-0.7.4 → methodproof-0.7.6}/methodproof/hooks/openclaw/handler.ts +0 -0
  41. {methodproof-0.7.4 → methodproof-0.7.6}/methodproof/hooks/openclaw_install.py +0 -0
  42. {methodproof-0.7.4 → methodproof-0.7.6}/methodproof/hooks/opencode_plugin.js +0 -0
  43. {methodproof-0.7.4 → methodproof-0.7.6}/methodproof/hooks/wrappers.py +0 -0
  44. {methodproof-0.7.4 → methodproof-0.7.6}/methodproof/integrity.py +0 -0
  45. {methodproof-0.7.4 → methodproof-0.7.6}/methodproof/kdf.py +0 -0
  46. {methodproof-0.7.4 → methodproof-0.7.6}/methodproof/keychain.py +0 -0
  47. {methodproof-0.7.4 → methodproof-0.7.6}/methodproof/live.py +0 -0
  48. {methodproof-0.7.4 → methodproof-0.7.6}/methodproof/lock.py +0 -0
  49. {methodproof-0.7.4 → methodproof-0.7.6}/methodproof/mcp.py +0 -0
  50. {methodproof-0.7.4 → methodproof-0.7.6}/methodproof/proxy.py +0 -0
  51. {methodproof-0.7.4 → methodproof-0.7.6}/methodproof/proxy_daemon.py +0 -0
  52. {methodproof-0.7.4 → methodproof-0.7.6}/methodproof/repos.py +0 -0
  53. {methodproof-0.7.4 → methodproof-0.7.6}/methodproof/skills/methodproof/SKILL.md +0 -0
  54. {methodproof-0.7.4 → methodproof-0.7.6}/methodproof/viewer.py +0 -0
  55. {methodproof-0.7.4 → methodproof-0.7.6}/methodproof/wordlist.py +0 -0
  56. {methodproof-0.7.4 → methodproof-0.7.6}/test_windows_compat.py +0 -0
  57. {methodproof-0.7.4 → methodproof-0.7.6}/tests/__init__.py +0 -0
  58. {methodproof-0.7.4 → methodproof-0.7.6}/tests/test_analysis.py +0 -0
  59. {methodproof-0.7.4 → methodproof-0.7.6}/tests/test_graph.py +0 -0
  60. {methodproof-0.7.4 → methodproof-0.7.6}/tests/test_hooks.py +0 -0
  61. {methodproof-0.7.4 → methodproof-0.7.6}/tests/test_live.py +0 -0
  62. {methodproof-0.7.4 → methodproof-0.7.6}/tests/test_openclaw_hooks.py +0 -0
  63. {methodproof-0.7.4 → methodproof-0.7.6}/tests/test_store.py +0 -0
  64. {methodproof-0.7.4 → methodproof-0.7.6}/tests/test_wrappers.py +0 -0
  65. {methodproof-0.7.4 → methodproof-0.7.6}/uv.lock +0 -0
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.7.6] — 2026-04-08
4
+
5
+ ### Added
6
+ - **Metadata compression** — event metadata stored as zlib-compressed BLOBs in SQLite (~80% storage reduction for journal-mode sessions). Existing uncompressed rows are silently migrated on next startup.
7
+ - **Gzip transfer encoding** — `mp push` sends gzip-compressed request bodies to the platform. Reduces upload bandwidth ~70% for large batches.
8
+
3
9
  ## [0.7.1] — 2026-04-07
4
10
 
5
11
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: methodproof
3
- Version: 0.7.4
3
+ Version: 0.7.6
4
4
  Summary: See how you code. Capture and visualize your engineering process.
5
5
  License-Expression: Apache-2.0
6
6
  License-File: LICENSE
@@ -60,7 +60,7 @@ methodproof view # explore your session in the browser
60
60
  - **Environment profiling** — structural analysis of your AI dev environment (instruction files, tool counts, MCP servers) captured at session start
61
61
  - **Outcome metrics** — first-shot apply rate, follow-up sequences, phase transitions computed at session end
62
62
  - **Granular consent** — 10 standard capture categories + 1 premium, each independently toggled. Nothing records without your opt-in
63
- - **Local-first** — SQLite database at `~/.methodproof/`, `chmod 600`. No network calls unless you choose
63
+ - **Local-first** — SQLite database at `~/.methodproof/`, `chmod 600`, zlib-compressed metadata. No network calls unless you choose
64
64
  - **Live streaming** — `methodproof start --live` streams events to the platform in real-time over WebSocket
65
65
  - **Integrity verification** — hash-chained events + Ed25519 attestation prove sessions haven't been tampered with
66
66
  - **E2E encryption** — optional company-held AES-256-GCM encryption the platform cannot decrypt
@@ -99,7 +99,7 @@ flowchart TB
99
99
  CHAIN --> BUF["Batched Flush"]
100
100
  end
101
101
 
102
- BUF --> DB[("SQLite WAL")]
102
+ BUF -->|"zlib compress"| DB[("SQLite WAL")]
103
103
  BUF -.->|"--live"| WS["WebSocket Stream"]
104
104
 
105
105
  subgraph KEYS["Key Vault"]
@@ -115,7 +115,7 @@ flowchart TB
115
115
 
116
116
  subgraph PUSH["PUSH PROTOCOL"]
117
117
  direction TB
118
- DB --> BATCH["Batched Upload"]
118
+ DB -->|"gzip"| BATCH["Batched Upload"]
119
119
  BATCH --> BIND["Session Binding: HMAC over session metadata"]
120
120
  BIND --> SIGN["Ed25519 Sign: session summary"]
121
121
  end
@@ -153,18 +153,24 @@ flowchart TB
153
153
  | Command | What it does |
154
154
  |---------|-------------|
155
155
  | `init` | Interactive consent selector, install hooks, create data directory |
156
- | `start [--dir .] [--tags t1,t2] [--public] [--live]` | Start recording |
156
+ | `start [--dir .] [--tags t1,t2] [--public] [--live] [--journal] [--e2e]` | Start recording |
157
157
  | `stop` | Stop recording, build process graph |
158
158
  | `view [session_id]` | Open session graph in browser |
159
159
  | `log` | List sessions with sync status, visibility, tags |
160
- | `login` | Authenticate with the platform |
161
- | `push [session_id]` | Upload session |
162
- | `publish [session_id]` | Set public + push |
160
+ | `login [--api-url URL]` | Authenticate with the platform |
161
+ | `push [session_id] [--local]` | Upload session (`--local` targets localhost:8000) |
162
+ | `publish [session_id] [--anonymous]` | Set public + push (redaction applied) |
163
163
  | `tag <session_id> <tags>` | Add tags |
164
164
  | `delete <session_id> [-f]` | Delete session and all its data |
165
- | `consent` | Change capture categories |
165
+ | `consent` | Change capture, research, and redaction settings |
166
166
  | `review` | Inspect session data before pushing |
167
- | `update` | Check for and install CLI updates |
167
+ | `journal on/off/status` | Toggle journal mode (full content capture) |
168
+ | `e2e on/off/status/recover/release` | Manage personal E2E encryption keys |
169
+ | `extension pair/status/install` | Browser extension pairing |
170
+ | `proxy start/stop/status/cert` | Local AI API proxy (deep capture) |
171
+ | `update [--auto/--no-auto]` | Check for and install CLI updates |
172
+ | `lock [--purge]` | Destroy local encryption key (recoverable) |
173
+ | `uninstall [--keep-sessions]` | Remove all hooks, data, and config |
168
174
 
169
175
  ## Privacy & Consent
170
176
 
@@ -307,6 +313,25 @@ methodproof start
307
313
 
308
314
  **Git commits** are detected automatically — only commits in a git repo rooted at (or above) the watch directory are captured.
309
315
 
316
+ ## Local Development
317
+
318
+ Push sessions to a local API for testing:
319
+
320
+ ```bash
321
+ # One-time: login to your local API
322
+ mp login --api-url http://localhost:8000
323
+
324
+ # Push with --local flag (overrides stored URL for this command)
325
+ mp push --local
326
+
327
+ # Or set the env var (works with any command)
328
+ METHODPROOF_API_URL=http://localhost:8000 mp push
329
+ ```
330
+
331
+ `--local` is a shorthand for `http://localhost:8000`. It does not clobber your production token — it only overrides the API URL for that invocation.
332
+
333
+ You still need a valid JWT from the local API. Run `mp login --api-url http://localhost:8000` once to authenticate against your local platform.
334
+
310
335
  ## Data Directory
311
336
 
312
337
  `~/.methodproof/`
@@ -45,7 +45,7 @@ methodproof view # explore your session in the browser
45
45
  - **Environment profiling** — structural analysis of your AI dev environment (instruction files, tool counts, MCP servers) captured at session start
46
46
  - **Outcome metrics** — first-shot apply rate, follow-up sequences, phase transitions computed at session end
47
47
  - **Granular consent** — 10 standard capture categories + 1 premium, each independently toggled. Nothing records without your opt-in
48
- - **Local-first** — SQLite database at `~/.methodproof/`, `chmod 600`. No network calls unless you choose
48
+ - **Local-first** — SQLite database at `~/.methodproof/`, `chmod 600`, zlib-compressed metadata. No network calls unless you choose
49
49
  - **Live streaming** — `methodproof start --live` streams events to the platform in real-time over WebSocket
50
50
  - **Integrity verification** — hash-chained events + Ed25519 attestation prove sessions haven't been tampered with
51
51
  - **E2E encryption** — optional company-held AES-256-GCM encryption the platform cannot decrypt
@@ -84,7 +84,7 @@ flowchart TB
84
84
  CHAIN --> BUF["Batched Flush"]
85
85
  end
86
86
 
87
- BUF --> DB[("SQLite WAL")]
87
+ BUF -->|"zlib compress"| DB[("SQLite WAL")]
88
88
  BUF -.->|"--live"| WS["WebSocket Stream"]
89
89
 
90
90
  subgraph KEYS["Key Vault"]
@@ -100,7 +100,7 @@ flowchart TB
100
100
 
101
101
  subgraph PUSH["PUSH PROTOCOL"]
102
102
  direction TB
103
- DB --> BATCH["Batched Upload"]
103
+ DB -->|"gzip"| BATCH["Batched Upload"]
104
104
  BATCH --> BIND["Session Binding: HMAC over session metadata"]
105
105
  BIND --> SIGN["Ed25519 Sign: session summary"]
106
106
  end
@@ -138,18 +138,24 @@ flowchart TB
138
138
  | Command | What it does |
139
139
  |---------|-------------|
140
140
  | `init` | Interactive consent selector, install hooks, create data directory |
141
- | `start [--dir .] [--tags t1,t2] [--public] [--live]` | Start recording |
141
+ | `start [--dir .] [--tags t1,t2] [--public] [--live] [--journal] [--e2e]` | Start recording |
142
142
  | `stop` | Stop recording, build process graph |
143
143
  | `view [session_id]` | Open session graph in browser |
144
144
  | `log` | List sessions with sync status, visibility, tags |
145
- | `login` | Authenticate with the platform |
146
- | `push [session_id]` | Upload session |
147
- | `publish [session_id]` | Set public + push |
145
+ | `login [--api-url URL]` | Authenticate with the platform |
146
+ | `push [session_id] [--local]` | Upload session (`--local` targets localhost:8000) |
147
+ | `publish [session_id] [--anonymous]` | Set public + push (redaction applied) |
148
148
  | `tag <session_id> <tags>` | Add tags |
149
149
  | `delete <session_id> [-f]` | Delete session and all its data |
150
- | `consent` | Change capture categories |
150
+ | `consent` | Change capture, research, and redaction settings |
151
151
  | `review` | Inspect session data before pushing |
152
- | `update` | Check for and install CLI updates |
152
+ | `journal on/off/status` | Toggle journal mode (full content capture) |
153
+ | `e2e on/off/status/recover/release` | Manage personal E2E encryption keys |
154
+ | `extension pair/status/install` | Browser extension pairing |
155
+ | `proxy start/stop/status/cert` | Local AI API proxy (deep capture) |
156
+ | `update [--auto/--no-auto]` | Check for and install CLI updates |
157
+ | `lock [--purge]` | Destroy local encryption key (recoverable) |
158
+ | `uninstall [--keep-sessions]` | Remove all hooks, data, and config |
153
159
 
154
160
  ## Privacy & Consent
155
161
 
@@ -292,6 +298,25 @@ methodproof start
292
298
 
293
299
  **Git commits** are detected automatically — only commits in a git repo rooted at (or above) the watch directory are captured.
294
300
 
301
+ ## Local Development
302
+
303
+ Push sessions to a local API for testing:
304
+
305
+ ```bash
306
+ # One-time: login to your local API
307
+ mp login --api-url http://localhost:8000
308
+
309
+ # Push with --local flag (overrides stored URL for this command)
310
+ mp push --local
311
+
312
+ # Or set the env var (works with any command)
313
+ METHODPROOF_API_URL=http://localhost:8000 mp push
314
+ ```
315
+
316
+ `--local` is a shorthand for `http://localhost:8000`. It does not clobber your production token — it only overrides the API URL for that invocation.
317
+
318
+ You still need a valid JWT from the local API. Run `mp login --api-url http://localhost:8000` once to authenticate against your local platform.
319
+
295
320
  ## Data Directory
296
321
 
297
322
  `~/.methodproof/`
@@ -1,3 +1,3 @@
1
1
  """MethodProof — see how you code."""
2
2
 
3
- __version__ = "0.7.4"
3
+ __version__ = "0.7.6"
@@ -606,8 +606,9 @@ def compute_outcomes(session_id: str) -> dict[str, Any]:
606
606
  if e["type"] not in prompt_types:
607
607
  continue
608
608
  try:
609
- meta = json.loads(e["metadata"])
610
- except (json.JSONDecodeError, TypeError) as exc:
609
+ from methodproof.store import _decompress_meta
610
+ meta = _decompress_meta(e["metadata"])
611
+ except (json.JSONDecodeError, TypeError, Exception) as exc:
611
612
  _warn("analysis.metadata_parse_failed", event_id=e["id"] if hasattr(e, "__getitem__") else "?", error=str(exc))
612
613
  continue
613
614
  intent = meta.get("sa_intent")
@@ -466,6 +466,7 @@ def _print_commands() -> None:
466
466
  print(f" {_G}mp start --live{R} Stream your graph privately (only you can view)")
467
467
  print(f" {_G}mp start --live-public{R} Stream your graph publicly (shareable link)")
468
468
  print(f" {_G}mp start --journal{R} Full content capture (2 free credits, then Pro)")
469
+ print(f" {_G}mp start --e2e{R} Encrypt session with your personal key {_D}(Pro){R}")
469
470
  print(f" {_G}mp journal on{R} Enable persistent journal mode")
470
471
  print(f" {_G}mp journal status{R} Check journal mode and remaining credits")
471
472
  print()
@@ -476,15 +477,29 @@ def _print_commands() -> None:
476
477
  print()
477
478
  print(f" {_W}SHARE{R}")
478
479
  print(f" {_Y}mp push{R} {_D}[id]{R} Upload privately to your account")
480
+ print(f" {_Y}mp push --local{R} Push to local dev API {_D}(localhost:8000){R}")
479
481
  print(f" {_Y}mp publish{R} {_D}[id]{R} Make session public (redaction applied)")
480
482
  print(f" {_Y}mp publish --anonymous{R} Public but identity hidden {_D}(Pro){R}")
481
483
  print(f" {_Y}mp tag{R} {_D}<id> <tags>{R} Tag a session")
482
484
  print()
485
+ print(f" {_W}ENCRYPTION{R}")
486
+ print(f" {_C}mp e2e on{R} Enable E2E encryption (generates key on first use)")
487
+ print(f" {_C}mp e2e off{R} Disable E2E mode (key stays in keychain)")
488
+ print(f" {_C}mp e2e status{R} Show E2E mode and key status")
489
+ print(f" {_C}mp e2e recover{R} Recover key from recovery passphrase")
490
+ print(f" {_C}mp e2e release{R} {_D}<id>{R} Release a session from E2E encryption")
491
+ print()
483
492
  print(f" {_W}EXTENSION{R}")
484
493
  print(f" {_C}mp extension pair{R} Pair browser extension to active session")
485
494
  print(f" {_C}mp extension status{R} Check extension connection")
486
495
  print(f" {_C}mp extension install{R} Open Chrome Web Store")
487
496
  print()
497
+ print(f" {_W}PROXY{R}")
498
+ print(f" {_C}mp proxy start{R} Start local AI API proxy {_D}(deep capture){R}")
499
+ print(f" {_C}mp proxy stop{R} Stop proxy")
500
+ print(f" {_C}mp proxy status{R} Show proxy status")
501
+ print(f" {_C}mp proxy cert{R} CA certificate install instructions")
502
+ print()
488
503
  print(f" {_W}ACCOUNT{R}")
489
504
  print(f" {_M}mp login{R} Connect to platform (opens browser)")
490
505
  print(f" {_M}mp consent{R} Change capture, research, and redaction settings")
@@ -497,6 +512,9 @@ def _print_commands() -> None:
497
512
  print(f" {_M}mp update --no-auto{R} Toggle auto-update off")
498
513
  print(f" {_M}mp uninstall{R} Remove all hooks, data, and config")
499
514
  print()
515
+ print(f" {_W}ENVIRONMENT{R}")
516
+ print(f" {_D}METHODPROOF_API_URL{R} Override API endpoint {_D}(e.g. http://localhost:8000){R}")
517
+ print()
500
518
  print(f" {_D}To view this at any time run: mp help{R}\n")
501
519
 
502
520
 
@@ -504,7 +522,12 @@ def _print_commands_plain() -> None:
504
522
  print(" RECORD")
505
523
  print(" mp start Start recording a session")
506
524
  print(" mp stop Stop recording, build process graph")
507
- print(" mp start --live Stream your graph in real time")
525
+ print(" mp start --live Stream your graph privately")
526
+ print(" mp start --live-public Stream your graph publicly (shareable link)")
527
+ print(" mp start --journal Full content capture (2 free credits, then Pro)")
528
+ print(" mp start --e2e Encrypt session with your personal key (Pro)")
529
+ print(" mp journal on Enable persistent journal mode")
530
+ print(" mp journal status Check journal mode and remaining credits")
508
531
  print()
509
532
  print(" REVIEW")
510
533
  print(" mp view [id] View session graph in browser")
@@ -513,15 +536,29 @@ def _print_commands_plain() -> None:
513
536
  print()
514
537
  print(" SHARE")
515
538
  print(" mp push [id] Upload privately to your account")
539
+ print(" mp push --local Push to local dev API (localhost:8000)")
516
540
  print(" mp publish [id] Make session public (redaction applied)")
517
541
  print(" mp publish --anonymous Public but identity hidden (Pro)")
518
542
  print(" mp tag <id> <tags> Tag a session")
519
543
  print()
544
+ print(" ENCRYPTION")
545
+ print(" mp e2e on Enable E2E encryption (generates key on first use)")
546
+ print(" mp e2e off Disable E2E mode (key stays in keychain)")
547
+ print(" mp e2e status Show E2E mode and key status")
548
+ print(" mp e2e recover Recover key from recovery passphrase")
549
+ print(" mp e2e release <id> Release a session from E2E encryption")
550
+ print()
520
551
  print(" EXTENSION")
521
552
  print(" mp extension pair Pair browser extension to active session")
522
553
  print(" mp extension status Check extension connection")
523
554
  print(" mp extension install Open Chrome Web Store")
524
555
  print()
556
+ print(" PROXY")
557
+ print(" mp proxy start Start local AI API proxy (deep capture)")
558
+ print(" mp proxy stop Stop proxy")
559
+ print(" mp proxy status Show proxy status")
560
+ print(" mp proxy cert CA certificate install instructions")
561
+ print()
525
562
  print(" ACCOUNT")
526
563
  print(" mp login Connect to platform (opens browser)")
527
564
  print(" mp consent Change capture, research, and redaction settings")
@@ -534,6 +571,9 @@ def _print_commands_plain() -> None:
534
571
  print(" mp update --no-auto Toggle auto-update off")
535
572
  print(" mp uninstall Remove all hooks, data, and config")
536
573
  print()
574
+ print(" ENVIRONMENT")
575
+ print(" METHODPROOF_API_URL Override API endpoint (e.g. http://localhost:8000)")
576
+ print()
537
577
  print(" To view this at any time run: mp help\n")
538
578
 
539
579
 
@@ -5,7 +5,7 @@ import time
5
5
  import uuid
6
6
  from typing import Any
7
7
 
8
- from methodproof.store import _db
8
+ from methodproof.store import _compress_meta, _db, _decompress_meta
9
9
 
10
10
 
11
11
  def build(session_id: str) -> dict[str, int]:
@@ -53,7 +53,7 @@ def build(session_id: str) -> dict[str, int]:
53
53
 
54
54
  # Resources
55
55
  for e in events:
56
- meta = json.loads(e["metadata"])
56
+ meta = _decompress_meta(e["metadata"])
57
57
  if e["type"] in ("llm_prompt", "llm_completion") and "model" in meta:
58
58
  _ensure_resource(db, "llm_model", meta["model"])
59
59
  stats["resources"] += 1
@@ -63,7 +63,7 @@ def build(session_id: str) -> dict[str, int]:
63
63
 
64
64
  # Artifacts
65
65
  for e in events:
66
- meta = json.loads(e["metadata"])
66
+ meta = _decompress_meta(e["metadata"])
67
67
  if e["type"] in ("file_create", "file_edit") and "path" in meta:
68
68
  _ensure_artifact(db, meta["path"], meta.get("size", 0))
69
69
  stats["artifacts"] += 1
@@ -84,7 +84,7 @@ def build(session_id: str) -> dict[str, int]:
84
84
  "(id, session_id, type, timestamp, duration_ms, metadata) "
85
85
  "VALUES (?, ?, ?, ?, ?, ?)",
86
86
  (uuid.uuid4().hex, session_id, "prompt_outcomes",
87
- time.time(), 0, json.dumps(outcomes)),
87
+ time.time(), 0, _compress_meta(outcomes)),
88
88
  )
89
89
  stats["outcomes"] = 1
90
90
  except Exception as exc:
@@ -100,7 +100,7 @@ def _link(
100
100
  rel: str, window_sec: int, match_model: bool = False,
101
101
  ) -> int:
102
102
  model_clause = (
103
- "AND json_extract(s.metadata, '$.model') = json_extract(t.metadata, '$.model')"
103
+ "AND json_extract(mp_json(s.metadata), '$.model') = json_extract(mp_json(t.metadata), '$.model')"
104
104
  if match_model else ""
105
105
  )
106
106
  sql = f"""
@@ -123,9 +123,9 @@ def _link_pasted(db: object, sid: str) -> int:
123
123
  FROM events s JOIN events t ON t.session_id = s.session_id
124
124
  WHERE s.session_id = ? AND s.type = 'browser_copy' AND t.type = 'file_edit'
125
125
  AND t.timestamp > s.timestamp AND (t.timestamp - s.timestamp) <= 30
126
- AND abs(json_extract(t.metadata, '$.lines_added') * 40.0
127
- - json_extract(s.metadata, '$.text_length'))
128
- < json_extract(s.metadata, '$.text_length') * 0.2
126
+ AND abs(json_extract(mp_json(t.metadata), '$.lines_added') * 40.0
127
+ - json_extract(mp_json(s.metadata), '$.text_length'))
128
+ < json_extract(mp_json(s.metadata), '$.text_length') * 0.2
129
129
  """
130
130
  return db.execute(sql, (sid,)).rowcount
131
131
 
@@ -135,20 +135,20 @@ def _link_action_resources(db: object, sid: str) -> None:
135
135
  db.execute("""
136
136
  INSERT OR IGNORE INTO action_resources (action_id, resource_id, relation_type, metadata)
137
137
  SELECT e.id, r.id, 'SENT_TO', '{}'
138
- FROM events e JOIN resources r ON r.identifier = json_extract(e.metadata, '$.model')
138
+ FROM events e JOIN resources r ON r.identifier = json_extract(mp_json(e.metadata), '$.model')
139
139
  WHERE e.session_id = ? AND e.type = 'llm_prompt' AND r.type = 'llm_model'
140
140
  """, (sid,))
141
141
  db.execute("""
142
142
  INSERT OR IGNORE INTO action_resources (action_id, resource_id, relation_type, metadata)
143
143
  SELECT e.id, r.id, 'CONSUMED', '{}'
144
- FROM events e JOIN resources r ON r.identifier = json_extract(e.metadata, '$.model')
144
+ FROM events e JOIN resources r ON r.identifier = json_extract(mp_json(e.metadata), '$.model')
145
145
  WHERE e.session_id = ? AND e.type = 'llm_completion' AND r.type = 'llm_model'
146
146
  """, (sid,))
147
147
  # Agent gateway links
148
148
  db.execute("""
149
149
  INSERT OR IGNORE INTO action_resources (action_id, resource_id, relation_type, metadata)
150
150
  SELECT e.id, r.id, 'SENT_TO', '{}'
151
- FROM events e JOIN resources r ON r.identifier = json_extract(e.metadata, '$.gateway')
151
+ FROM events e JOIN resources r ON r.identifier = json_extract(mp_json(e.metadata), '$.gateway')
152
152
  WHERE e.session_id = ? AND e.type = 'agent_prompt' AND r.type = 'agent_gateway'
153
153
  """, (sid,))
154
154
 
@@ -158,13 +158,13 @@ def _link_action_artifacts(db: object, sid: str) -> None:
158
158
  db.execute("""
159
159
  INSERT OR IGNORE INTO action_artifacts (action_id, artifact_id, relation_type)
160
160
  SELECT e.id, a.id, 'PRODUCED'
161
- FROM events e JOIN artifacts a ON a.path = json_extract(e.metadata, '$.path')
161
+ FROM events e JOIN artifacts a ON a.path = json_extract(mp_json(e.metadata), '$.path')
162
162
  WHERE e.session_id = ? AND e.type = 'file_create'
163
163
  """, (sid,))
164
164
  db.execute("""
165
165
  INSERT OR IGNORE INTO action_artifacts (action_id, artifact_id, relation_type)
166
166
  SELECT e.id, a.id, 'MODIFIED'
167
- FROM events e JOIN artifacts a ON a.path = json_extract(e.metadata, '$.path')
167
+ FROM events e JOIN artifacts a ON a.path = json_extract(mp_json(e.metadata), '$.path')
168
168
  WHERE e.session_id = ? AND e.type = 'file_edit'
169
169
  """, (sid,))
170
170
 
@@ -1,9 +1,8 @@
1
1
  """Encrypt existing plaintext events in local DB after key setup."""
2
2
 
3
- import json
4
-
5
3
  from methodproof import store
6
4
  from methodproof.crypto import SENSITIVE_FIELDS, encrypt_field
5
+ from methodproof.store import _compress_meta, _decompress_meta
7
6
 
8
7
 
9
8
  def migrate_encrypt(db_key: bytes) -> int:
@@ -18,7 +17,7 @@ def migrate_encrypt(db_key: bytes) -> int:
18
17
  encrypted = 0
19
18
  batch = []
20
19
  for row in rows:
21
- meta = json.loads(row["metadata"])
20
+ meta = _decompress_meta(row["metadata"])
22
21
  changed = False
23
22
  for field in SENSITIVE_FIELDS:
24
23
  val = meta.get(field)
@@ -26,7 +25,7 @@ def migrate_encrypt(db_key: bytes) -> int:
26
25
  meta[field] = encrypt_field(val, db_key)
27
26
  changed = True
28
27
  if changed:
29
- batch.append((json.dumps(meta), row["id"]))
28
+ batch.append((_compress_meta(meta), row["id"]))
30
29
  encrypted += 1
31
30
  if len(batch) >= 500:
32
31
  _flush_batch(db, batch)
@@ -4,6 +4,7 @@ import json
4
4
  import sqlite3
5
5
  import time
6
6
  import uuid
7
+ import zlib
7
8
  from typing import Any
8
9
 
9
10
  from methodproof import config
@@ -62,12 +63,25 @@ CREATE TABLE IF NOT EXISTS action_artifacts (
62
63
  _conn: sqlite3.Connection | None = None
63
64
 
64
65
 
66
+ def _sqlite_decompress(raw: bytes | str | None) -> str | None:
67
+ """SQLite UDF: decompress zlib BLOBs to JSON text for json_extract()."""
68
+ if raw is None:
69
+ return None
70
+ if isinstance(raw, bytes):
71
+ try:
72
+ return zlib.decompress(raw).decode()
73
+ except zlib.error:
74
+ return raw.decode() if raw else "{}"
75
+ return raw
76
+
77
+
65
78
  def _db() -> sqlite3.Connection:
66
79
  global _conn
67
80
  if _conn is None:
68
81
  _conn = sqlite3.connect(str(config.DB_PATH), check_same_thread=False, timeout=10)
69
82
  _conn.execute("PRAGMA journal_mode=WAL")
70
83
  _conn.row_factory = sqlite3.Row
84
+ _conn.create_function("mp_json", 1, _sqlite_decompress)
71
85
  return _conn
72
86
 
73
87
 
@@ -113,9 +127,30 @@ def _migrate() -> None:
113
127
  db.execute("DELETE FROM action_artifacts")
114
128
  db.execute("DELETE FROM artifacts WHERE rowid NOT IN (SELECT MIN(rowid) FROM artifacts GROUP BY path)")
115
129
  db.execute("CREATE UNIQUE INDEX IF NOT EXISTS idx_artifacts_path ON artifacts(path)")
130
+ # Compress legacy uncompressed metadata (TEXT → zlib BLOB)
131
+ _migrate_compress_metadata(db)
116
132
  db.commit()
117
133
 
118
134
 
135
+ def _migrate_compress_metadata(db: sqlite3.Connection) -> None:
136
+ """Silently compress any uncompressed TEXT metadata rows to zlib BLOBs."""
137
+ rows = db.execute("SELECT id, metadata FROM events WHERE typeof(metadata) = 'text'").fetchall()
138
+ if not rows:
139
+ return
140
+ batch, skipped = [], 0
141
+ for r in rows:
142
+ try:
143
+ compressed = zlib.compress(r["metadata"].encode())
144
+ batch.append((compressed, r["id"]))
145
+ except Exception:
146
+ skipped += 1
147
+ if batch:
148
+ db.executemany("UPDATE events SET metadata = ? WHERE id = ?", batch)
149
+ if skipped:
150
+ import sys
151
+ sys.stderr.write(f"[methodproof] migration: compressed {len(batch)} events, skipped {skipped}\n")
152
+
153
+
119
154
  def create_session(
120
155
  session_id: str, watch_dir: str,
121
156
  repo_url: str | None = None, tags: str = "[]", visibility: str = "private",
@@ -142,14 +177,27 @@ def complete_session(session_id: str) -> None:
142
177
  db.commit()
143
178
 
144
179
 
180
+ def _compress_meta(meta: dict[str, Any]) -> bytes:
181
+ return zlib.compress(json.dumps(meta, default=str).encode())
182
+
183
+
184
+ def _decompress_meta(raw: bytes | str) -> dict[str, Any]:
185
+ if isinstance(raw, str):
186
+ return json.loads(raw)
187
+ try:
188
+ return json.loads(zlib.decompress(raw))
189
+ except zlib.error:
190
+ return json.loads(raw)
191
+
192
+
145
193
  def insert_events(session_id: str, events: list[dict[str, Any]]) -> None:
146
194
  db = _db()
147
195
  rows = []
148
196
  for e in events:
149
197
  try:
150
- meta = json.dumps(e.get("metadata", {}), default=str)
198
+ meta = _compress_meta(e.get("metadata", {}))
151
199
  except (TypeError, ValueError):
152
- meta = "{}"
200
+ meta = _compress_meta({})
153
201
  rows.append((
154
202
  e["id"], session_id, e["type"], e["timestamp"],
155
203
  e.get("duration_ms", 0), meta,
@@ -176,7 +224,12 @@ def get_events(session_id: str) -> list[dict[str, Any]]:
176
224
  "SELECT * FROM events WHERE session_id = ? ORDER BY timestamp",
177
225
  (session_id,),
178
226
  ).fetchall()
179
- return [dict(r) for r in rows]
227
+ result = []
228
+ for r in rows:
229
+ d = dict(r)
230
+ d["metadata"] = json.dumps(_decompress_meta(d["metadata"]))
231
+ result.append(d)
232
+ return result
180
233
 
181
234
 
182
235
  def get_graph(session_id: str) -> dict[str, Any]:
@@ -184,7 +237,7 @@ def get_graph(session_id: str) -> dict[str, Any]:
184
237
  events = get_events(session_id)
185
238
  nodes = [{"id": e["id"], "type": "Action", "label": e["type"],
186
239
  "properties": {"timestamp": e["timestamp"], "duration_ms": e["duration_ms"],
187
- "metadata": json.loads(e["metadata"])}}
240
+ "metadata": _decompress_meta(e["metadata"])}}
188
241
  for e in events]
189
242
 
190
243
  edges = []
@@ -1,5 +1,6 @@
1
1
  """Push local sessions to the MethodProof platform."""
2
2
 
3
+ import gzip
3
4
  import json
4
5
  import urllib.error
5
6
  import urllib.request
@@ -30,8 +31,13 @@ def _raw_request(
30
31
  method: str, url: str, token: str,
31
32
  body: dict[str, Any] | None = None,
32
33
  ) -> dict[str, Any]:
33
- data = json.dumps(body).encode() if body else None
34
+ if body is not None:
35
+ data = gzip.compress(json.dumps(body).encode())
36
+ else:
37
+ data = None
34
38
  headers = {"Content-Type": "application/json", "Authorization": f"Bearer {token}"}
39
+ if data is not None:
40
+ headers["Content-Encoding"] = "gzip"
35
41
  req = urllib.request.Request(url, data=data, headers=headers, method=method)
36
42
  with urllib.request.urlopen(req, timeout=15) as resp:
37
43
  return json.loads(resp.read())
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "methodproof"
3
- version = "0.7.4"
3
+ version = "0.7.6"
4
4
  description = "See how you code. Capture and visualize your engineering process."
5
5
  requires-python = ">=3.11"
6
6
  dependencies = ["watchdog>=4.0", "websocket-client>=1.7", "cryptography>=43.0", "keyring>=25.0"]
@@ -106,10 +106,12 @@ def test_bip39_known_vector():
106
106
 
107
107
 
108
108
  def test_bip39_bad_checksum():
109
- phrase = entropy_to_phrase(os.urandom(16))
109
+ # Use fixed entropy so the checksum corruption is deterministic
110
+ phrase = entropy_to_phrase(b"\x00" * 16)
110
111
  words = phrase.split()
111
- words[-1] = words[0] # corrupt checksum
112
- with pytest.raises(ValueError, match="checksum|Unknown"):
112
+ # Flip the last word to a word that changes the checksum bits
113
+ words[-1] = "zone"
114
+ with pytest.raises(ValueError, match="checksum"):
113
115
  phrase_to_entropy(" ".join(words))
114
116
 
115
117
 
@@ -257,7 +259,8 @@ def test_migrate_encrypts_plaintext():
257
259
  count = migrate_encrypt(key)
258
260
  assert count == 1
259
261
  row = store._db().execute("SELECT metadata FROM events WHERE id = 'e1'").fetchone()
260
- assert "e2e:v1:" in json.loads(row["metadata"])["prompt_text"]
262
+ meta = store._decompress_meta(row["metadata"])
263
+ assert "e2e:v1:" in meta["prompt_text"]
261
264
 
262
265
 
263
266
  def test_migrate_idempotent():
File without changes
File without changes
File without changes