methodproof 0.5.0__tar.gz → 0.5.2__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 (66) hide show
  1. {methodproof-0.5.0 → methodproof-0.5.2}/CHANGELOG.md +13 -0
  2. {methodproof-0.5.0 → methodproof-0.5.2}/PKG-INFO +1 -1
  3. methodproof-0.5.2/methodproof/_daemon.py +114 -0
  4. {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/agents/music.py +4 -2
  5. {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/bridge.py +3 -1
  6. {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/cli.py +71 -79
  7. {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/graph.py +3 -2
  8. methodproof-0.5.2/methodproof/hooks/claude_code.py +86 -0
  9. {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/keychain.py +4 -4
  10. {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/live.py +5 -3
  11. {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/mcp.py +2 -2
  12. {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/proxy_daemon.py +7 -6
  13. {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/sync.py +5 -3
  14. {methodproof-0.5.0 → methodproof-0.5.2}/pyproject.toml +1 -1
  15. methodproof-0.5.2/uv.lock +1811 -0
  16. methodproof-0.5.0/methodproof/hooks/claude_code.py +0 -73
  17. methodproof-0.5.0/uv.lock +0 -272
  18. {methodproof-0.5.0 → methodproof-0.5.2}/.github/workflows/ci.yml +0 -0
  19. {methodproof-0.5.0 → methodproof-0.5.2}/.gitignore +0 -0
  20. {methodproof-0.5.0 → methodproof-0.5.2}/LICENSE +0 -0
  21. {methodproof-0.5.0 → methodproof-0.5.2}/README.md +0 -0
  22. {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/__init__.py +0 -0
  23. {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/__main__.py +0 -0
  24. {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/agents/__init__.py +0 -0
  25. {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/agents/base.py +0 -0
  26. {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/agents/terminal.py +0 -0
  27. {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/agents/watcher.py +0 -0
  28. {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/analysis.py +0 -0
  29. {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/binding.py +0 -0
  30. {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/bip39.py +0 -0
  31. {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/config.py +0 -0
  32. {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/crypto.py +0 -0
  33. {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/hook.py +0 -0
  34. {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/hooks/__init__.py +0 -0
  35. {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/hooks/claude_code.sh +0 -0
  36. {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/hooks/cline_hook.sh +0 -0
  37. {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/hooks/codex_hook.sh +0 -0
  38. {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/hooks/gemini_hook.sh +0 -0
  39. {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/hooks/install.py +0 -0
  40. {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/hooks/kiro_hook.sh +0 -0
  41. {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/hooks/mcp_register.py +0 -0
  42. {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/hooks/openclaw/HOOK.md +0 -0
  43. {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/hooks/openclaw/handler.ts +0 -0
  44. {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/hooks/openclaw_install.py +0 -0
  45. {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/hooks/opencode_plugin.js +0 -0
  46. {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/hooks/wrappers.py +0 -0
  47. {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/integrity.py +0 -0
  48. {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/kdf.py +0 -0
  49. {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/lock.py +0 -0
  50. {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/migrate_db.py +0 -0
  51. {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/proxy.py +0 -0
  52. {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/repos.py +0 -0
  53. {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/skills/methodproof/SKILL.md +0 -0
  54. {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/store.py +0 -0
  55. {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/viewer.py +0 -0
  56. {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/wordlist.py +0 -0
  57. {methodproof-0.5.0 → methodproof-0.5.2}/test_windows_compat.py +0 -0
  58. {methodproof-0.5.0 → methodproof-0.5.2}/tests/__init__.py +0 -0
  59. {methodproof-0.5.0 → methodproof-0.5.2}/tests/test_analysis.py +0 -0
  60. {methodproof-0.5.0 → methodproof-0.5.2}/tests/test_graph.py +0 -0
  61. {methodproof-0.5.0 → methodproof-0.5.2}/tests/test_hooks.py +0 -0
  62. {methodproof-0.5.0 → methodproof-0.5.2}/tests/test_live.py +0 -0
  63. {methodproof-0.5.0 → methodproof-0.5.2}/tests/test_openclaw_hooks.py +0 -0
  64. {methodproof-0.5.0 → methodproof-0.5.2}/tests/test_security.py +0 -0
  65. {methodproof-0.5.0 → methodproof-0.5.2}/tests/test_store.py +0 -0
  66. {methodproof-0.5.0 → methodproof-0.5.2}/tests/test_wrappers.py +0 -0
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.5.1] — 2026-04-06
4
+
5
+ ### Fixed
6
+ - macOS daemon segfault: replaced `os.fork()` with `subprocess.Popen()` — CoreFoundation crash after fork killed all capture agents, producing 0 events
7
+ - Hook type mismatches: `task_created`→`task_start`, `task_completed`→`task_end`, added required `tool` field to all hook metadata
8
+ - Unmapped hook events dropped instead of sent as invalid `claude_code_event` type (rejected entire batch)
9
+ - Daemon output logged to `~/.methodproof/daemon.log` instead of `/dev/null`
10
+ - ~20 silent `except Exception: pass` blocks replaced with structured logging across bridge, hooks, live streaming, sync, keychain, MCP, and proxy
11
+
12
+ ### Changed
13
+ - Daemon health check on startup — immediate error if daemon exits
14
+ - Extension status check shows actual error on failure
15
+
3
16
  ## [0.3.4] — 2026-04-05
4
17
 
5
18
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: methodproof
3
- Version: 0.5.0
3
+ Version: 0.5.2
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
@@ -0,0 +1,114 @@
1
+ """Daemon process for methodproof capture — spawned by cmd_start via subprocess.
2
+
3
+ Uses subprocess.Popen (fork+exec) instead of os.fork() to avoid macOS
4
+ CoreFoundation segfaults when forking a multi-threaded Python 3.12+ process.
5
+ """
6
+
7
+ import signal
8
+ import sys
9
+ import threading
10
+ import time
11
+
12
+ from methodproof import config, store, graph
13
+ from methodproof.agents import base
14
+
15
+ PIDFILE = config.DIR / "methodproof.pid"
16
+
17
+
18
+ def main() -> None:
19
+ sid = sys.argv[1]
20
+ watch_dir = sys.argv[2]
21
+
22
+ cfg = config.load()
23
+ capture = cfg.get("capture", {})
24
+ live_url = cfg.get("_live_url", "")
25
+
26
+ # Clean up transient key
27
+ if "_live_url" in cfg:
28
+ del cfg["_live_url"]
29
+ config.save(cfg)
30
+
31
+ base.init(sid, live=bool(live_url))
32
+
33
+ # Environment profile (was done pre-fork in old code, now done in daemon)
34
+ if capture.get("environment_analysis", True):
35
+ try:
36
+ from methodproof.analysis import scan_environment
37
+ env_profile = scan_environment(watch_dir)
38
+ base.emit("environment_profile", env_profile)
39
+ except Exception as exc:
40
+ base.log("warning", "environment_scan.failed", error=str(exc))
41
+
42
+ stop_event = threading.Event()
43
+ threads: list[threading.Thread] = []
44
+
45
+ files_enabled = (
46
+ capture.get("file_changes", True)
47
+ or capture.get("git_diffs", True)
48
+ or capture.get("git_commits", True)
49
+ )
50
+ if files_enabled:
51
+ from methodproof.agents import watcher
52
+ threads.append(threading.Thread(
53
+ target=watcher.start, args=(watch_dir, stop_event), daemon=True,
54
+ ))
55
+
56
+ if capture.get("terminal_commands", True) or capture.get("test_results", True):
57
+ from methodproof.agents import terminal
58
+ threads.append(threading.Thread(
59
+ target=terminal.start, args=(stop_event,), daemon=True,
60
+ ))
61
+
62
+ if capture.get("browser", True):
63
+ from methodproof import bridge
64
+ threads.append(threading.Thread(
65
+ target=bridge.start,
66
+ args=(sid, stop_event, 9877,
67
+ cfg.get("token", ""), cfg.get("api_url", ""), cfg.get("e2e_key", "")),
68
+ daemon=True,
69
+ ))
70
+
71
+ if capture.get("music", True):
72
+ from methodproof.agents import music
73
+ threads.append(threading.Thread(
74
+ target=music.start, args=(stop_event,), daemon=True,
75
+ ))
76
+
77
+ def _shutdown(sig_num: int, frame: object) -> None:
78
+ stop_event.set()
79
+ try:
80
+ if live_url:
81
+ from methodproof import live as live_mod
82
+ live_mod.stop()
83
+ base.flush()
84
+ store.complete_session(sid)
85
+ graph.build(sid)
86
+ except Exception as exc:
87
+ base.log("error", "daemon.shutdown_cleanup_failed", error=str(exc))
88
+ try:
89
+ cfg_now = config.load()
90
+ cfg_now["active_session"] = None
91
+ config.save(cfg_now)
92
+ except Exception as exc:
93
+ base.log("error", "daemon.config_cleanup_failed", error=str(exc))
94
+ PIDFILE.unlink(missing_ok=True)
95
+ sys.exit(0)
96
+
97
+ for t in threads:
98
+ t.start()
99
+ signal.signal(signal.SIGINT, _shutdown)
100
+ signal.signal(signal.SIGTERM, _shutdown)
101
+
102
+ base.log("info", "daemon.started", session_id=sid, watch_dir=watch_dir, agents=len(threads))
103
+
104
+ try:
105
+ while not stop_event.is_set():
106
+ time.sleep(5)
107
+ base.flush()
108
+ except Exception as exc:
109
+ base.log("error", "daemon.loop_crashed", error=str(exc))
110
+ _shutdown(0, None)
111
+
112
+
113
+ if __name__ == "__main__":
114
+ main()
@@ -39,7 +39,8 @@ def _get_now_playing_macos() -> dict[str, str] | None:
39
39
  if len(parts) != 3 or not parts[0]:
40
40
  return None
41
41
  return {"track": parts[0], "artist": parts[1], "player": parts[2]}
42
- except Exception:
42
+ except Exception as exc:
43
+ base.log("debug", "music.macos_poll_failed", error=str(exc))
43
44
  return None
44
45
 
45
46
 
@@ -58,7 +59,8 @@ def _get_now_playing_linux() -> dict[str, str] | None:
58
59
  return {"track": parts[1], "artist": parts[0], "player": player_map.get(parts[2].lower(), "unknown")}
59
60
  except FileNotFoundError:
60
61
  return None
61
- except Exception:
62
+ except Exception as exc:
63
+ base.log("debug", "music.linux_poll_failed", error=str(exc))
62
64
  return None
63
65
 
64
66
 
@@ -73,7 +73,9 @@ def generate_pair_token(session_id: str, api_token: str, api_base: str, e2e_key:
73
73
 
74
74
  class _Handler(BaseHTTPRequestHandler):
75
75
  def log_message(self, fmt: str, *args: Any) -> None:
76
- pass
76
+ # Log errors (4xx/5xx) to structured log; suppress normal request chatter
77
+ if args and str(args[0]).startswith(("4", "5")):
78
+ base.log("warning", "bridge.http_error", status=args[0], path=self.path)
77
79
 
78
80
  def do_GET(self) -> None:
79
81
  if self.path == "/session":
@@ -907,8 +907,8 @@ def cmd_start(args: argparse.Namespace) -> None:
907
907
  from methodproof.sync import _request
908
908
  anchor = _request("POST", f"/sessions/{sid}/anchor", cfg["api_url"], cfg["token"])
909
909
  store.update_anchor(sid, anchor["anchor_ts"], anchor["signature"])
910
- except Exception:
911
- pass # offline — no anchor, lower trust score
910
+ except Exception as exc:
911
+ print(f"Anchor: skipped ({exc})")
912
912
 
913
913
  from methodproof.agents import base
914
914
  live_ok = False
@@ -950,21 +950,68 @@ def cmd_start(args: argparse.Namespace) -> None:
950
950
  print("Journal mode ON for this session (full content capture).")
951
951
  config.save(cfg)
952
952
 
953
- base.init(sid, live=bool(live_url))
953
+ # Save live_url for daemon subprocess to pick up
954
+ if live_url:
955
+ cfg["_live_url"] = live_url
956
+ config.save(cfg)
954
957
 
958
+ active = [k for k, v in capture.items() if v]
959
+ print(f"\n{_banner()}")
960
+ print(f"Recording: {sid[:8]}")
961
+ print(f"Watching: {watch_dir}")
962
+ if repo_url:
963
+ print(f"Repo: {repo_url}")
964
+ print(f"Capture: {', '.join(active)}")
965
+ if capture.get("browser", True):
966
+ print(f"Bridge: http://localhost:9877")
967
+ if live_url:
968
+ print(f"Live: {live_url}")
969
+
970
+ # Daemonize: spawn a subprocess (safe on macOS — avoids CoreFoundation segfault from os.fork)
971
+ if sys.platform != "win32":
972
+ import subprocess as _sp
973
+ daemon_log = config.DIR / "daemon.log"
974
+ log_fh = open(daemon_log, "a")
975
+ proc = _sp.Popen(
976
+ [sys.executable, "-m", "methodproof._daemon", sid, watch_dir],
977
+ start_new_session=True,
978
+ stdout=log_fh, stderr=log_fh, stdin=_sp.DEVNULL,
979
+ )
980
+ PIDFILE.write_text(str(proc.pid))
981
+ # Verify daemon is alive after brief startup
982
+ time.sleep(1)
983
+ if proc.poll() is not None:
984
+ print(f"ERROR: Daemon exited immediately (code {proc.returncode}). Check {daemon_log}")
985
+ PIDFILE.unlink(missing_ok=True)
986
+ sys.exit(1)
987
+ # Check extension auto-discovery (extension polls every ~6s)
988
+ if capture.get("browser", True):
989
+ import urllib.request as _ur
990
+ time.sleep(7)
991
+ try:
992
+ with _ur.urlopen("http://127.0.0.1:9877/extension-status", timeout=2) as resp:
993
+ ext_data = json.loads(resp.read())
994
+ if ext_data.get("paired"):
995
+ print("Extension: connected")
996
+ else:
997
+ print("Extension: not detected — run `mp extension pair` or install from store")
998
+ except Exception as exc:
999
+ print(f"Extension: not detected ({exc})")
1000
+ print("Run `mp stop` to finish.")
1001
+ return
1002
+
1003
+ # Windows: foreground mode (no subprocess daemonization)
1004
+ from methodproof.agents import base as _base
1005
+ _base.init(sid, live=bool(live_url))
955
1006
  if capture.get("environment_analysis", True):
956
- from methodproof.analysis import scan_environment
957
1007
  try:
1008
+ from methodproof.analysis import scan_environment
958
1009
  env_profile = scan_environment(watch_dir)
959
- base.emit("environment_profile", env_profile)
960
- except Exception:
961
- base.log("warning", "environment_scan.failed")
962
-
1010
+ _base.emit("environment_profile", env_profile)
1011
+ except Exception as exc:
1012
+ _base.log("warning", "environment_scan.failed", error=str(exc))
963
1013
  stop_event = threading.Event()
964
-
965
1014
  threads: list[threading.Thread] = []
966
-
967
- # File watcher — if any file/git category is enabled
968
1015
  files_enabled = (
969
1016
  capture.get("file_changes", True)
970
1017
  or capture.get("git_diffs", True)
@@ -973,100 +1020,39 @@ def cmd_start(args: argparse.Namespace) -> None:
973
1020
  if files_enabled:
974
1021
  from methodproof.agents import watcher
975
1022
  threads.append(threading.Thread(target=watcher.start, args=(watch_dir, stop_event), daemon=True))
976
-
977
- # Terminal monitor — if terminal or test categories enabled
978
1023
  if capture.get("terminal_commands", True) or capture.get("test_results", True):
979
1024
  from methodproof.agents import terminal
980
1025
  threads.append(threading.Thread(target=terminal.start, args=(stop_event,), daemon=True))
981
-
982
- # Bridge — if browser category enabled
983
1026
  if capture.get("browser", True):
984
1027
  from methodproof import bridge
985
1028
  threads.append(threading.Thread(target=bridge.start, args=(
986
1029
  sid, stop_event, 9877,
987
1030
  cfg.get("token", ""), cfg.get("api_url", ""), cfg.get("e2e_key", ""),
988
1031
  ), daemon=True))
989
-
990
- # Music — if music category enabled
991
1032
  if capture.get("music", True):
992
1033
  from methodproof.agents import music
993
1034
  threads.append(threading.Thread(target=music.start, args=(stop_event,), daemon=True))
994
1035
 
995
- active = [k for k, v in capture.items() if v]
996
- print(f"\n{_banner()}")
997
- print(f"Recording: {sid[:8]}")
998
- print(f"Watching: {watch_dir}")
999
- if repo_url:
1000
- print(f"Repo: {repo_url}")
1001
- print(f"Capture: {', '.join(active)}")
1002
- if capture.get("browser", True):
1003
- print(f"Bridge: http://localhost:9877")
1004
- if live_url:
1005
- print(f"Live: {live_url}")
1006
-
1007
1036
  def _shutdown(sig: int, frame: object) -> None:
1008
1037
  stop_event.set()
1009
1038
  try:
1010
1039
  if live_url:
1011
1040
  from methodproof import live as live_mod
1012
1041
  live_mod.stop()
1013
- base.flush()
1042
+ _base.flush()
1014
1043
  store.complete_session(sid)
1015
1044
  graph.build(sid)
1016
- except Exception:
1017
- pass
1045
+ except Exception as exc:
1046
+ _base.log("error", "shutdown.cleanup_failed", error=str(exc))
1018
1047
  try:
1019
1048
  cfg_now = config.load()
1020
1049
  cfg_now["active_session"] = None
1021
1050
  config.save(cfg_now)
1022
- except Exception:
1023
- pass
1051
+ except Exception as exc:
1052
+ _base.log("error", "shutdown.config_cleanup_failed", error=str(exc))
1024
1053
  PIDFILE.unlink(missing_ok=True)
1025
1054
  sys.exit(0)
1026
1055
 
1027
- # Daemonize: fork into background so the terminal is free
1028
- if sys.platform != "win32":
1029
- child = os.fork()
1030
- if child > 0:
1031
- # Parent: write child PID and exit
1032
- PIDFILE.write_text(str(child))
1033
- # Brief pause for extension auto-discovery (extension polls every ~6s in dev)
1034
- if capture.get("browser", True):
1035
- import urllib.request as _ur
1036
- time.sleep(8)
1037
- try:
1038
- with _ur.urlopen("http://127.0.0.1:9877/extension-status", timeout=2) as resp:
1039
- ext_data = json.loads(resp.read())
1040
- if ext_data.get("paired"):
1041
- print(f"Extension: connected")
1042
- else:
1043
- print(f"Extension: not detected — run `mp extension pair` or install from store")
1044
- except Exception:
1045
- print(f"Extension: not detected")
1046
- print("Run `mp stop` to finish.")
1047
- return
1048
- # Child: detach and run in background
1049
- os.setsid()
1050
- # Redirect stdio to /dev/null so writes don't fail after terminal closes
1051
- devnull = os.open(os.devnull, os.O_RDWR)
1052
- os.dup2(devnull, 0)
1053
- os.dup2(devnull, 1)
1054
- os.dup2(devnull, 2)
1055
- os.close(devnull)
1056
- store.reset_connection()
1057
- for t in threads:
1058
- t.start()
1059
- signal.signal(signal.SIGINT, _shutdown)
1060
- signal.signal(signal.SIGTERM, _shutdown)
1061
- try:
1062
- while not stop_event.is_set():
1063
- time.sleep(5)
1064
- base.flush()
1065
- except Exception:
1066
- _shutdown(0, None)
1067
- return
1068
-
1069
- # Windows: foreground mode (no fork)
1070
1056
  for t in threads:
1071
1057
  t.start()
1072
1058
  signal.signal(signal.SIGINT, _shutdown)
@@ -1077,7 +1063,7 @@ def cmd_start(args: argparse.Namespace) -> None:
1077
1063
  stopfile.unlink(missing_ok=True)
1078
1064
  _shutdown(0, None)
1079
1065
  time.sleep(5)
1080
- base.flush()
1066
+ _base.flush()
1081
1067
 
1082
1068
 
1083
1069
  def cmd_stop(args: argparse.Namespace) -> None:
@@ -1160,10 +1146,16 @@ def cmd_login(args: argparse.Namespace) -> None:
1160
1146
  result = _request("POST", "/auth/cli/start", api, "")
1161
1147
  code = result["code"]
1162
1148
  auth_url = result["auth_url"]
1149
+ user_code = result.get("user_code", "")
1150
+ verification_url = result.get("verification_url", "")
1163
1151
 
1164
1152
  print(f"\nOpening browser to sign in...\n")
1165
1153
  print(f" {auth_url}\n")
1166
- print("If the browser doesn't open, copy the URL above.\n")
1154
+ if user_code and verification_url:
1155
+ print(f"Can't open a browser? Visit {verification_url} and enter:\n")
1156
+ print(f" {user_code}\n")
1157
+ else:
1158
+ print("If the browser doesn't open, copy the URL above.\n")
1167
1159
  webbrowser.open(auth_url)
1168
1160
 
1169
1161
  # Poll until approved or expired
@@ -87,8 +87,9 @@ def build(session_id: str) -> dict[str, int]:
87
87
  time.time(), 0, json.dumps(outcomes)),
88
88
  )
89
89
  stats["outcomes"] = 1
90
- except Exception:
91
- pass
90
+ except Exception as exc:
91
+ from methodproof.agents.base import log
92
+ log("warning", "graph.outcomes_failed", session_id=session_id, error=str(exc))
92
93
 
93
94
  db.commit()
94
95
  return stats
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env python3
2
+ """MethodProof hook for Claude Code — cross-platform Python equivalent of claude_code.sh.
3
+
4
+ Receives JSON on stdin. Posts to local bridge. Fails silently.
5
+ Uses only stdlib — no jq, no curl dependency.
6
+ """
7
+
8
+ import json
9
+ import sys
10
+ import time
11
+ import urllib.request
12
+
13
+ try:
14
+ from methodproof.analysis import analyze_prompt, compose_summary
15
+ except ImportError:
16
+ analyze_prompt = lambda _: {}
17
+ compose_summary = lambda _: ""
18
+
19
+ def _build_prompt_meta(text: str) -> dict:
20
+ sa = analyze_prompt(text)
21
+ sa["prompt_length"] = len(text)
22
+ sa["prompt_summary"] = compose_summary(sa)
23
+ return sa
24
+
25
+
26
+ _TYPE_MAP = {
27
+ "UserPromptSubmit": "user_prompt",
28
+ "PreToolUse": "tool_call",
29
+ "PostToolUse": "tool_result",
30
+ "SubagentStart": "agent_launch",
31
+ "SubagentStop": "agent_complete",
32
+ "TaskCreated": "task_start",
33
+ "TaskCompleted": "task_end",
34
+ "SessionStart": "claude_session_start",
35
+ }
36
+
37
+ _TOOL = "claude_code"
38
+
39
+ _META_EXTRACTORS = {
40
+ "UserPromptSubmit": lambda d: {
41
+ "tool": _TOOL, "prompt_preview": _build_prompt_meta(d.get("prompt") or "").get("prompt_summary", ""),
42
+ "prompt_length": len(d.get("prompt") or ""),
43
+ },
44
+ "PreToolUse": lambda d: {"tool": _TOOL, "tool_name": d.get("tool_name", "unknown")},
45
+ "PostToolUse": lambda d: {"tool": _TOOL, "tool_name": d.get("tool_name", "unknown"), "success": True},
46
+ "SubagentStart": lambda d: {"tool": _TOOL, "agent_type": d.get("agent_type", "unknown"), "agent_id": d.get("agent_id", "")},
47
+ "SubagentStop": lambda d: {"tool": _TOOL, "agent_type": d.get("agent_type", "unknown"), "agent_id": d.get("agent_id", "")},
48
+ "TaskCreated": lambda d: {"tool": _TOOL, "task_id": d.get("task_id", ""), "subject": d.get("subject", "")},
49
+ "TaskCompleted": lambda d: {"tool": _TOOL, "task_id": d.get("task_id", "")},
50
+ "SessionStart": lambda d: {"tool": _TOOL, "session_id": d.get("session_id", ""), "cwd": d.get("cwd", "")},
51
+ }
52
+
53
+
54
+ def main() -> None:
55
+ try:
56
+ data = json.load(sys.stdin)
57
+ except (json.JSONDecodeError, ValueError):
58
+ return
59
+
60
+ event = data.get("hook_event_name", "unknown")
61
+ etype = _TYPE_MAP.get(event)
62
+ if not etype:
63
+ return # Unmapped hook event — drop rather than send invalid type
64
+ extractor = _META_EXTRACTORS.get(event)
65
+ meta = extractor(data) if extractor else {"tool": _TOOL}
66
+ ts = time.time()
67
+
68
+ payload = json.dumps({"events": [{"type": etype, "timestamp": ts, "metadata": meta}]}).encode()
69
+ req = urllib.request.Request(
70
+ "http://localhost:9877/events", data=payload,
71
+ headers={"Content-Type": "application/json"}, method="POST",
72
+ )
73
+ try:
74
+ urllib.request.urlopen(req, timeout=1)
75
+ except Exception as exc:
76
+ import pathlib
77
+ log = pathlib.Path.home() / ".methodproof" / "hook_errors.log"
78
+ try:
79
+ with open(log, "a") as f:
80
+ f.write(f"{time.time():.0f} hook.post_failed type={etype} error={exc}\n")
81
+ except OSError:
82
+ pass
83
+
84
+
85
+ if __name__ == "__main__":
86
+ main()
@@ -32,8 +32,8 @@ def load_secret(account_id: str) -> bytes | None:
32
32
  val = keyring.get_password(_SERVICE, account_id)
33
33
  if val:
34
34
  return bytes.fromhex(val)
35
- except Exception:
36
- pass
35
+ except Exception as exc:
36
+ sys.stderr.write(f"keychain.load_failed account={account_id} error={exc}\n")
37
37
  return _load_file()
38
38
 
39
39
 
@@ -42,8 +42,8 @@ def delete_secret(account_id: str) -> None:
42
42
  try:
43
43
  import keyring
44
44
  keyring.delete_password(_SERVICE, account_id)
45
- except Exception:
46
- pass
45
+ except Exception as exc:
46
+ sys.stderr.write(f"keychain.delete_failed account={account_id} error={exc}\n")
47
47
  path = _fallback_path()
48
48
  if path.exists():
49
49
  path.unlink()
@@ -95,7 +95,8 @@ def send(event: dict[str, Any]) -> None:
95
95
  try:
96
96
  _send_queue.put_nowait(json.dumps(event, default=str))
97
97
  except queue.Full:
98
- pass
98
+ from methodproof.agents.base import log
99
+ log("warning", "live.queue_full", dropped_type=event.get("type", "?"))
99
100
 
100
101
 
101
102
  def stop() -> None:
@@ -106,8 +107,9 @@ def stop() -> None:
106
107
  if _ws:
107
108
  try:
108
109
  _ws.close()
109
- except Exception:
110
- pass
110
+ except Exception as exc:
111
+ from methodproof.agents.base import log
112
+ log("warning", "live.close_failed", error=str(exc))
111
113
  _ws = None
112
114
 
113
115
 
@@ -30,8 +30,8 @@ def _emit(event_type: str, metadata: dict[str, Any]) -> None:
30
30
  headers={"Content-Type": "application/json"}, method="POST",
31
31
  )
32
32
  urllib.request.urlopen(req, timeout=2)
33
- except Exception:
34
- pass
33
+ except Exception as exc:
34
+ sys.stderr.write(f"mcp.bridge_post_failed type={event_type} error={exc}\n")
35
35
 
36
36
 
37
37
  def serve() -> None:
@@ -35,13 +35,13 @@ _STRIP_HEADERS = {"authorization", "x-api-key", "api-key", "x-goog-api-key"}
35
35
 
36
36
 
37
37
  def _post_event(event_type: str, metadata: dict) -> None:
38
- """Post event to bridge. Fail silently."""
38
+ """Post event to bridge. Logs on failure."""
39
39
  body = json.dumps({"events": [{"type": event_type, "timestamp": time.time(), "metadata": metadata}]}).encode()
40
40
  req = urllib.request.Request(BRIDGE_URL, data=body, headers={"Content-Type": "application/json"})
41
41
  try:
42
42
  urllib.request.urlopen(req, timeout=1)
43
- except Exception:
44
- pass
43
+ except Exception as exc:
44
+ sys.stderr.write(f"proxy.bridge_post_failed type={event_type} error={exc}\n")
45
45
 
46
46
 
47
47
  def _is_ai_domain(domain: str) -> bool:
@@ -68,8 +68,8 @@ def _decode_via_binary(provider: str, direction: str, body: dict, latency_ms: fl
68
68
  )
69
69
  if result.returncode == 0 and result.stdout.strip():
70
70
  return json.loads(result.stdout)
71
- except Exception:
72
- pass
71
+ except Exception as exc:
72
+ sys.stderr.write(f"proxy.decoder_failed provider={provider} dir={direction} error={exc}\n")
73
73
  return None
74
74
 
75
75
 
@@ -83,7 +83,8 @@ def _match_provider(domain: str, path: str) -> str | None:
83
83
  )
84
84
  name = result.stdout.strip()
85
85
  return name if name and name != "null" else None
86
- except Exception:
86
+ except Exception as exc:
87
+ sys.stderr.write(f"proxy.match_failed domain={domain} path={path} error={exc}\n")
87
88
  return None
88
89
 
89
90
 
@@ -42,7 +42,9 @@ def _refresh_token(api_url: str, refresh: str) -> tuple[str, str] | None:
42
42
  try:
43
43
  result = _raw_request("POST", f"{api_url}/auth/refresh", "", {"refresh_token": refresh})
44
44
  return result["access_token"], result["refresh_token"]
45
- except Exception:
45
+ except Exception as exc:
46
+ from methodproof.agents.base import log
47
+ log("warning", "sync.refresh_token_failed", error=str(exc))
46
48
  return None
47
49
 
48
50
 
@@ -69,8 +71,8 @@ def _request(
69
71
  if exc.fp:
70
72
  try:
71
73
  detail = json.loads(exc.read()).get("detail", "")
72
- except Exception:
73
- pass
74
+ except Exception as parse_err:
75
+ detail = f"(response unreadable: {parse_err})"
74
76
  raise SystemExit(f"API error {exc.code}: {detail}") from None
75
77
 
76
78
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "methodproof"
3
- version = "0.5.0"
3
+ version = "0.5.2"
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"]