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.
- {methodproof-0.5.0 → methodproof-0.5.2}/CHANGELOG.md +13 -0
- {methodproof-0.5.0 → methodproof-0.5.2}/PKG-INFO +1 -1
- methodproof-0.5.2/methodproof/_daemon.py +114 -0
- {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/agents/music.py +4 -2
- {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/bridge.py +3 -1
- {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/cli.py +71 -79
- {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/graph.py +3 -2
- methodproof-0.5.2/methodproof/hooks/claude_code.py +86 -0
- {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/keychain.py +4 -4
- {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/live.py +5 -3
- {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/mcp.py +2 -2
- {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/proxy_daemon.py +7 -6
- {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/sync.py +5 -3
- {methodproof-0.5.0 → methodproof-0.5.2}/pyproject.toml +1 -1
- methodproof-0.5.2/uv.lock +1811 -0
- methodproof-0.5.0/methodproof/hooks/claude_code.py +0 -73
- methodproof-0.5.0/uv.lock +0 -272
- {methodproof-0.5.0 → methodproof-0.5.2}/.github/workflows/ci.yml +0 -0
- {methodproof-0.5.0 → methodproof-0.5.2}/.gitignore +0 -0
- {methodproof-0.5.0 → methodproof-0.5.2}/LICENSE +0 -0
- {methodproof-0.5.0 → methodproof-0.5.2}/README.md +0 -0
- {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/__init__.py +0 -0
- {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/__main__.py +0 -0
- {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/agents/__init__.py +0 -0
- {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/agents/base.py +0 -0
- {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/agents/terminal.py +0 -0
- {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/agents/watcher.py +0 -0
- {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/analysis.py +0 -0
- {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/binding.py +0 -0
- {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/bip39.py +0 -0
- {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/config.py +0 -0
- {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/crypto.py +0 -0
- {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/hook.py +0 -0
- {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/hooks/__init__.py +0 -0
- {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/hooks/claude_code.sh +0 -0
- {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/hooks/cline_hook.sh +0 -0
- {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/hooks/codex_hook.sh +0 -0
- {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/hooks/gemini_hook.sh +0 -0
- {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/hooks/install.py +0 -0
- {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/hooks/kiro_hook.sh +0 -0
- {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/hooks/mcp_register.py +0 -0
- {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/hooks/openclaw/HOOK.md +0 -0
- {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/hooks/openclaw/handler.ts +0 -0
- {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/hooks/openclaw_install.py +0 -0
- {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/hooks/opencode_plugin.js +0 -0
- {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/hooks/wrappers.py +0 -0
- {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/integrity.py +0 -0
- {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/kdf.py +0 -0
- {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/lock.py +0 -0
- {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/migrate_db.py +0 -0
- {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/proxy.py +0 -0
- {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/repos.py +0 -0
- {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/skills/methodproof/SKILL.md +0 -0
- {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/store.py +0 -0
- {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/viewer.py +0 -0
- {methodproof-0.5.0 → methodproof-0.5.2}/methodproof/wordlist.py +0 -0
- {methodproof-0.5.0 → methodproof-0.5.2}/test_windows_compat.py +0 -0
- {methodproof-0.5.0 → methodproof-0.5.2}/tests/__init__.py +0 -0
- {methodproof-0.5.0 → methodproof-0.5.2}/tests/test_analysis.py +0 -0
- {methodproof-0.5.0 → methodproof-0.5.2}/tests/test_graph.py +0 -0
- {methodproof-0.5.0 → methodproof-0.5.2}/tests/test_hooks.py +0 -0
- {methodproof-0.5.0 → methodproof-0.5.2}/tests/test_live.py +0 -0
- {methodproof-0.5.0 → methodproof-0.5.2}/tests/test_openclaw_hooks.py +0 -0
- {methodproof-0.5.0 → methodproof-0.5.2}/tests/test_security.py +0 -0
- {methodproof-0.5.0 → methodproof-0.5.2}/tests/test_store.py +0 -0
- {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
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
960
|
-
except Exception:
|
|
961
|
-
|
|
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
|
-
|
|
1042
|
+
_base.flush()
|
|
1014
1043
|
store.complete_session(sid)
|
|
1015
1044
|
graph.build(sid)
|
|
1016
|
-
except Exception:
|
|
1017
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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"]
|