methodproof 0.3.1__tar.gz → 0.3.3__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.3.3/CHANGELOG.md +89 -0
- methodproof-0.3.1/README.md → methodproof-0.3.3/PKG-INFO +33 -0
- methodproof-0.3.1/PKG-INFO → methodproof-0.3.3/README.md +18 -16
- {methodproof-0.3.1 → methodproof-0.3.3}/methodproof/agents/base.py +1 -0
- methodproof-0.3.3/methodproof/bridge.py +200 -0
- {methodproof-0.3.1 → methodproof-0.3.3}/methodproof/cli.py +220 -37
- {methodproof-0.3.1 → methodproof-0.3.3}/methodproof/hooks/claude_code.sh +10 -5
- {methodproof-0.3.1 → methodproof-0.3.3}/methodproof/live.py +4 -4
- {methodproof-0.3.1 → methodproof-0.3.3}/methodproof/store.py +11 -0
- methodproof-0.3.3/methodproof/viewer.py +141 -0
- {methodproof-0.3.1 → methodproof-0.3.3}/pyproject.toml +2 -3
- methodproof-0.3.3/tests/test_live.py +148 -0
- {methodproof-0.3.1 → methodproof-0.3.3}/uv.lock +4 -6
- methodproof-0.3.1/CHANGELOG.md +0 -20
- methodproof-0.3.1/methodproof/bridge.py +0 -86
- methodproof-0.3.1/methodproof/viewer.py +0 -239
- {methodproof-0.3.1 → methodproof-0.3.3}/.github/workflows/ci.yml +0 -0
- {methodproof-0.3.1 → methodproof-0.3.3}/.gitignore +0 -0
- {methodproof-0.3.1 → methodproof-0.3.3}/LICENSE +0 -0
- {methodproof-0.3.1 → methodproof-0.3.3}/methodproof/__init__.py +0 -0
- {methodproof-0.3.1 → methodproof-0.3.3}/methodproof/__main__.py +0 -0
- {methodproof-0.3.1 → methodproof-0.3.3}/methodproof/agents/__init__.py +0 -0
- {methodproof-0.3.1 → methodproof-0.3.3}/methodproof/agents/music.py +0 -0
- {methodproof-0.3.1 → methodproof-0.3.3}/methodproof/agents/terminal.py +0 -0
- {methodproof-0.3.1 → methodproof-0.3.3}/methodproof/agents/watcher.py +0 -0
- {methodproof-0.3.1 → methodproof-0.3.3}/methodproof/analysis.py +0 -0
- {methodproof-0.3.1 → methodproof-0.3.3}/methodproof/config.py +0 -0
- {methodproof-0.3.1 → methodproof-0.3.3}/methodproof/crypto.py +0 -0
- {methodproof-0.3.1 → methodproof-0.3.3}/methodproof/graph.py +0 -0
- {methodproof-0.3.1 → methodproof-0.3.3}/methodproof/hook.py +0 -0
- {methodproof-0.3.1 → methodproof-0.3.3}/methodproof/hooks/__init__.py +0 -0
- {methodproof-0.3.1 → methodproof-0.3.3}/methodproof/hooks/claude_code.py +0 -0
- {methodproof-0.3.1 → methodproof-0.3.3}/methodproof/hooks/install.py +0 -0
- {methodproof-0.3.1 → methodproof-0.3.3}/methodproof/hooks/openclaw/HOOK.md +0 -0
- {methodproof-0.3.1 → methodproof-0.3.3}/methodproof/hooks/openclaw/handler.ts +0 -0
- {methodproof-0.3.1 → methodproof-0.3.3}/methodproof/hooks/openclaw_install.py +0 -0
- {methodproof-0.3.1 → methodproof-0.3.3}/methodproof/hooks/wrappers.py +0 -0
- {methodproof-0.3.1 → methodproof-0.3.3}/methodproof/integrity.py +0 -0
- {methodproof-0.3.1 → methodproof-0.3.3}/methodproof/mcp.py +0 -0
- {methodproof-0.3.1 → methodproof-0.3.3}/methodproof/repos.py +0 -0
- {methodproof-0.3.1 → methodproof-0.3.3}/methodproof/skills/methodproof/SKILL.md +0 -0
- {methodproof-0.3.1 → methodproof-0.3.3}/methodproof/sync.py +0 -0
- {methodproof-0.3.1 → methodproof-0.3.3}/test_windows_compat.py +0 -0
- {methodproof-0.3.1 → methodproof-0.3.3}/tests/__init__.py +0 -0
- {methodproof-0.3.1 → methodproof-0.3.3}/tests/test_analysis.py +0 -0
- {methodproof-0.3.1 → methodproof-0.3.3}/tests/test_graph.py +0 -0
- {methodproof-0.3.1 → methodproof-0.3.3}/tests/test_hooks.py +0 -0
- {methodproof-0.3.1 → methodproof-0.3.3}/tests/test_openclaw_hooks.py +0 -0
- {methodproof-0.3.1 → methodproof-0.3.3}/tests/test_store.py +0 -0
- {methodproof-0.3.1 → methodproof-0.3.3}/tests/test_wrappers.py +0 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.3.3] — 2026-04-05
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- macOS hook timestamp: `date +%s.%3N` produced invalid JSON (literal `.3N`), silently dropping all tool_call, tool_result, agent_launch, and agent_complete events
|
|
7
|
+
- Stale session recovery: `mp start` now detects dead daemons and cleans up instead of blocking
|
|
8
|
+
- Bridge events now route through `base.emit()` for consent gating, hash chain integrity, and live streaming
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- `mp view` replaced with terminal-based session audit (no HTTP server)
|
|
12
|
+
- Recording threads start after fork (fixes silent 0-event sessions on macOS)
|
|
13
|
+
- Hook errors logged to `~/.methodproof/hook_errors.log` for inspection
|
|
14
|
+
- Consent-blocked events logged at debug level
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- Watch Scope section in README documenting directory scope and exclusion patterns
|
|
18
|
+
- `store.reset_connection()` for fork-safe SQLite WAL handling
|
|
19
|
+
|
|
20
|
+
## [0.3.2] — 2026-04-04
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
- `websocket-client` is now a default dependency (no longer requires `pip install methodproof[live]`)
|
|
24
|
+
- `mp start --live` prints a clickable dashboard URL instead of the API host
|
|
25
|
+
- `live.start()` returns the dashboard URL from the platform handshake
|
|
26
|
+
|
|
27
|
+
## [0.3.1] — 2026-04-04
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
- `mp extension pair` — pair browser extension to active session
|
|
31
|
+
- `mp extension status` — check extension connection
|
|
32
|
+
- `mp extension install` — open Chrome Web Store listing
|
|
33
|
+
- Bridge auto-discovery: extension auto-connects when CLI session starts
|
|
34
|
+
- Bridge `/pair/auto` endpoint for extension auto-pairing
|
|
35
|
+
- Bridge `/pair/register` + `/pair/ack` for manual pairing flow
|
|
36
|
+
- `mp start` now runs in the background (daemon mode on Unix)
|
|
37
|
+
- Extension status check on session start (connected/not detected)
|
|
38
|
+
- `SO_REUSEADDR` on bridge socket for clean restarts
|
|
39
|
+
|
|
40
|
+
### Changed
|
|
41
|
+
- Bridge accepts API credentials for auto-discovery passthrough
|
|
42
|
+
- `_shutdown` handler wrapped in try/except for robust daemon cleanup
|
|
43
|
+
|
|
44
|
+
## [0.3.0] — 2026-04-03
|
|
45
|
+
|
|
46
|
+
### Added
|
|
47
|
+
- Prompt structural analysis: 35 metadata dimensions extracted from AI prompts
|
|
48
|
+
- Environment profiling: structural scan of AI dev environment (instruction files, tool configs)
|
|
49
|
+
- `environment_analysis` consent category (default on, structural metadata only)
|
|
50
|
+
- Outcome metrics: correlation between prompt patterns and session results
|
|
51
|
+
- Consent review prompt when new capture categories exist after update
|
|
52
|
+
|
|
53
|
+
### Changed
|
|
54
|
+
- README rebrand: OG dark banner, hero process graph, brand-colored badges
|
|
55
|
+
|
|
56
|
+
### Fixed
|
|
57
|
+
- Unused import, silent failures, operator precedence, magic numbers (code review)
|
|
58
|
+
|
|
59
|
+
## [0.2.0] — 2026-03-30
|
|
60
|
+
|
|
61
|
+
### Added
|
|
62
|
+
- Browser login via device auth flow (`mp login` opens browser)
|
|
63
|
+
- Three-section interactive consent (capture, research, redaction)
|
|
64
|
+
- `mp uninstall` — remove all hooks, data, and config
|
|
65
|
+
- Color-coded command reference (`mp help`)
|
|
66
|
+
- Auto-refresh expired API tokens
|
|
67
|
+
- `mp update` — self-update from PyPI
|
|
68
|
+
- AI Agent Graph branding for prompt/response events
|
|
69
|
+
- Code capture consent category (Pro only, encrypted, default off)
|
|
70
|
+
- Windows compatibility (stop sentinel, icacls permissions)
|
|
71
|
+
- `--live` flag streams events to platform in real-time over WebSocket
|
|
72
|
+
- `live` optional dependency (`pip install methodproof[live]`)
|
|
73
|
+
- Free-tier full-spectrum consent unlocks 30-day rolling live stream
|
|
74
|
+
|
|
75
|
+
## [0.1.1] — 2026-03-28
|
|
76
|
+
|
|
77
|
+
### Fixed
|
|
78
|
+
- Added pytest dev dependency
|
|
79
|
+
|
|
80
|
+
## [0.1.0] — 2026-03-27
|
|
81
|
+
|
|
82
|
+
### Added
|
|
83
|
+
- Initial release
|
|
84
|
+
- `mp init/start/stop/view/log/push/publish/review/consent/tag/delete`
|
|
85
|
+
- File watcher, terminal monitor, music agent
|
|
86
|
+
- Local bridge for browser extension events
|
|
87
|
+
- Hash-chained events + Ed25519 attestation
|
|
88
|
+
- Full Spectrum messaging (all 10 categories)
|
|
89
|
+
- `mp mcp-serve` — MCP server for Claude Code
|
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: methodproof
|
|
3
|
+
Version: 0.3.3
|
|
4
|
+
Summary: See how you code. Capture and visualize your engineering process.
|
|
5
|
+
License-Expression: Apache-2.0
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Python: >=3.11
|
|
8
|
+
Requires-Dist: watchdog>=4.0
|
|
9
|
+
Requires-Dist: websocket-client>=1.7
|
|
10
|
+
Provides-Extra: e2e
|
|
11
|
+
Requires-Dist: cryptography>=43.0; extra == 'e2e'
|
|
12
|
+
Provides-Extra: signing
|
|
13
|
+
Requires-Dist: cryptography>=43.0; extra == 'signing'
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
1
16
|
<p align="center">
|
|
2
17
|
<img src="https://cdn.methodproof.com/og/og-primary-dark.png" alt="MethodProof — Engineering Process Intelligence" width="720" />
|
|
3
18
|
</p>
|
|
@@ -191,6 +206,24 @@ All sensitive metadata (prompts, completions, commands, output, diffs) is encryp
|
|
|
191
206
|
- **AI CLIs** — codex, gemini, aider command wrappers
|
|
192
207
|
- **MCP server** — registered with Claude Code for session/graph queries
|
|
193
208
|
|
|
209
|
+
## Watch Scope
|
|
210
|
+
|
|
211
|
+
`methodproof start` watches the **current directory recursively** (or the directory passed via `--dir`). Every file create, edit, and delete under that tree generates an event.
|
|
212
|
+
|
|
213
|
+
**Start in the right directory.** If you start in a monorepo root, you'll capture events from every subdirectory. If you start in a subdirectory, parent-level changes won't be recorded.
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
cd my-project # scope to this project
|
|
217
|
+
methodproof start
|
|
218
|
+
|
|
219
|
+
cd ~/code # ⚠️ captures ALL projects under ~/code
|
|
220
|
+
methodproof start
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
**Excluded patterns:** `__pycache__`, `.pyc`, `.git/`, `node_modules`, `.DS_Store`, `.swp`, temp files ending in `~`
|
|
224
|
+
|
|
225
|
+
**Git commits** are detected by polling `.git/refs/heads/` every 2 seconds — only commits in a git repo rooted at (or above) the watch directory are captured.
|
|
226
|
+
|
|
194
227
|
## Data Directory
|
|
195
228
|
|
|
196
229
|
`~/.methodproof/`
|
|
@@ -1,19 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: methodproof
|
|
3
|
-
Version: 0.3.1
|
|
4
|
-
Summary: See how you code. Capture and visualize your engineering process.
|
|
5
|
-
License-Expression: Apache-2.0
|
|
6
|
-
License-File: LICENSE
|
|
7
|
-
Requires-Python: >=3.11
|
|
8
|
-
Requires-Dist: watchdog>=4.0
|
|
9
|
-
Provides-Extra: e2e
|
|
10
|
-
Requires-Dist: cryptography>=43.0; extra == 'e2e'
|
|
11
|
-
Provides-Extra: live
|
|
12
|
-
Requires-Dist: websocket-client>=1.7; extra == 'live'
|
|
13
|
-
Provides-Extra: signing
|
|
14
|
-
Requires-Dist: cryptography>=43.0; extra == 'signing'
|
|
15
|
-
Description-Content-Type: text/markdown
|
|
16
|
-
|
|
17
1
|
<p align="center">
|
|
18
2
|
<img src="https://cdn.methodproof.com/og/og-primary-dark.png" alt="MethodProof — Engineering Process Intelligence" width="720" />
|
|
19
3
|
</p>
|
|
@@ -207,6 +191,24 @@ All sensitive metadata (prompts, completions, commands, output, diffs) is encryp
|
|
|
207
191
|
- **AI CLIs** — codex, gemini, aider command wrappers
|
|
208
192
|
- **MCP server** — registered with Claude Code for session/graph queries
|
|
209
193
|
|
|
194
|
+
## Watch Scope
|
|
195
|
+
|
|
196
|
+
`methodproof start` watches the **current directory recursively** (or the directory passed via `--dir`). Every file create, edit, and delete under that tree generates an event.
|
|
197
|
+
|
|
198
|
+
**Start in the right directory.** If you start in a monorepo root, you'll capture events from every subdirectory. If you start in a subdirectory, parent-level changes won't be recorded.
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
cd my-project # scope to this project
|
|
202
|
+
methodproof start
|
|
203
|
+
|
|
204
|
+
cd ~/code # ⚠️ captures ALL projects under ~/code
|
|
205
|
+
methodproof start
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
**Excluded patterns:** `__pycache__`, `.pyc`, `.git/`, `node_modules`, `.DS_Store`, `.swp`, temp files ending in `~`
|
|
209
|
+
|
|
210
|
+
**Git commits** are detected by polling `.git/refs/heads/` every 2 seconds — only commits in a git repo rooted at (or above) the watch directory are captured.
|
|
211
|
+
|
|
210
212
|
## Data Directory
|
|
211
213
|
|
|
212
214
|
`~/.methodproof/`
|
|
@@ -83,6 +83,7 @@ def emit(event_type: str, metadata: dict[str, Any]) -> None:
|
|
|
83
83
|
# Event-level consent gate
|
|
84
84
|
gate = _EVENT_GATES.get(event_type)
|
|
85
85
|
if gate and not _capture.get(gate, True):
|
|
86
|
+
log("debug", "emit.consent_blocked", type=event_type, gate=gate)
|
|
86
87
|
return
|
|
87
88
|
# Field-level consent gate — strip opted-out fields
|
|
88
89
|
for category, pairs in _FIELD_GATES.items():
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""Local HTTP bridge — accepts browser extension events into SQLite and handles pairing."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import secrets
|
|
5
|
+
import threading
|
|
6
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from methodproof.agents import base
|
|
10
|
+
|
|
11
|
+
_session_id = ""
|
|
12
|
+
_api_token = ""
|
|
13
|
+
_api_base = ""
|
|
14
|
+
_e2e_key = ""
|
|
15
|
+
_pairing: dict[str, Any] = {} # {token: {session_id, api_token, api_base, e2e_key, paired}}
|
|
16
|
+
_extension_paired = threading.Event()
|
|
17
|
+
MAX_BODY = 10 * 1024 * 1024 # 10 MB
|
|
18
|
+
|
|
19
|
+
PAIR_PAGE = """<!DOCTYPE html>
|
|
20
|
+
<html>
|
|
21
|
+
<head>
|
|
22
|
+
<meta charset="utf-8">
|
|
23
|
+
<title>MethodProof — Pair Extension</title>
|
|
24
|
+
<style>
|
|
25
|
+
body {{ font-family: Inter, system-ui, sans-serif; background: #faf9f7; color: #0a0a0a;
|
|
26
|
+
display: flex; justify-content: center; align-items: center; min-height: 100vh; margin: 0; }}
|
|
27
|
+
.card {{ text-align: center; padding: 48px; max-width: 400px; }}
|
|
28
|
+
h1 {{ font-size: 18px; margin: 0 0 8px; }}
|
|
29
|
+
h1 span {{ font-family: Laila, serif; font-weight: 400; }}
|
|
30
|
+
.status {{ font-size: 14px; color: #666; margin: 24px 0; }}
|
|
31
|
+
.paired {{ color: #2d7a42; font-weight: 600; }}
|
|
32
|
+
.session {{ font-family: 'IBM Plex Mono', monospace; font-size: 12px; color: #888;
|
|
33
|
+
background: #f0eeea; padding: 8px 12px; display: inline-block; }}
|
|
34
|
+
.spinner {{ display: inline-block; width: 16px; height: 16px; border: 2px solid #ddd;
|
|
35
|
+
border-top-color: #d93326; border-radius: 50%; animation: spin 0.8s linear infinite;
|
|
36
|
+
vertical-align: middle; margin-right: 8px; }}
|
|
37
|
+
@keyframes spin {{ to {{ transform: rotate(360deg); }} }}
|
|
38
|
+
</style>
|
|
39
|
+
</head>
|
|
40
|
+
<body>
|
|
41
|
+
<div class="card">
|
|
42
|
+
<h1><b>Method</b><span>Proof</span></h1>
|
|
43
|
+
<div class="session">{session_short}</div>
|
|
44
|
+
<div id="status" class="status"><span class="spinner"></span>Pairing extension...</div>
|
|
45
|
+
<div id="methodproof-pair-data"
|
|
46
|
+
data-session-id="{session_id}"
|
|
47
|
+
data-token="{api_token}"
|
|
48
|
+
data-api-base="{api_base}"
|
|
49
|
+
data-e2e-key="{e2e_key}"
|
|
50
|
+
style="display:none"></div>
|
|
51
|
+
</div>
|
|
52
|
+
<script>
|
|
53
|
+
window.addEventListener('methodproof-paired', function() {{
|
|
54
|
+
document.getElementById('status').className = 'status paired';
|
|
55
|
+
document.getElementById('status').innerHTML = '✓ Extension paired';
|
|
56
|
+
fetch('/pair/ack', {{method: 'POST', headers: {{'Content-Type': 'application/json'}},
|
|
57
|
+
body: JSON.stringify({{token: '{pair_token}'}}) }});
|
|
58
|
+
setTimeout(function() {{ window.close(); }}, 2000);
|
|
59
|
+
}});
|
|
60
|
+
</script>
|
|
61
|
+
</body>
|
|
62
|
+
</html>"""
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def generate_pair_token(session_id: str, api_token: str, api_base: str, e2e_key: str = "") -> str:
|
|
66
|
+
token = secrets.token_urlsafe(16)
|
|
67
|
+
_pairing[token] = {
|
|
68
|
+
"session_id": session_id, "api_token": api_token,
|
|
69
|
+
"api_base": api_base, "e2e_key": e2e_key, "paired": False,
|
|
70
|
+
}
|
|
71
|
+
return token
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class _Handler(BaseHTTPRequestHandler):
|
|
75
|
+
def log_message(self, fmt: str, *args: Any) -> None:
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
def do_GET(self) -> None:
|
|
79
|
+
if self.path == "/session":
|
|
80
|
+
self._json({"session_id": _session_id, "active": bool(_session_id)})
|
|
81
|
+
elif self.path.startswith("/pair?token="):
|
|
82
|
+
token = self.path.split("token=", 1)[1].split("&")[0]
|
|
83
|
+
data = _pairing.get(token)
|
|
84
|
+
if not data:
|
|
85
|
+
self.send_error(403, "Invalid or expired pairing token")
|
|
86
|
+
return
|
|
87
|
+
html = PAIR_PAGE.format(
|
|
88
|
+
session_id=data["session_id"],
|
|
89
|
+
session_short=data["session_id"][:8],
|
|
90
|
+
api_token=data["api_token"],
|
|
91
|
+
api_base=data["api_base"],
|
|
92
|
+
e2e_key=data["e2e_key"],
|
|
93
|
+
pair_token=token,
|
|
94
|
+
).encode()
|
|
95
|
+
self.send_response(200)
|
|
96
|
+
self.send_header("Content-Type", "text/html")
|
|
97
|
+
self._cors()
|
|
98
|
+
self.end_headers()
|
|
99
|
+
self.wfile.write(html)
|
|
100
|
+
elif self.path == "/pair/auto":
|
|
101
|
+
if not _session_id:
|
|
102
|
+
self._json({"active": False})
|
|
103
|
+
else:
|
|
104
|
+
self._json({
|
|
105
|
+
"active": True,
|
|
106
|
+
"session_id": _session_id,
|
|
107
|
+
"token": _api_token,
|
|
108
|
+
"api_base": _api_base,
|
|
109
|
+
"e2e_key": _e2e_key,
|
|
110
|
+
})
|
|
111
|
+
_extension_paired.set()
|
|
112
|
+
base.log("info", "extension.auto_paired", session_id=_session_id)
|
|
113
|
+
elif self.path == "/extension-status":
|
|
114
|
+
self._json({"paired": _extension_paired.is_set()})
|
|
115
|
+
else:
|
|
116
|
+
self.send_error(404)
|
|
117
|
+
|
|
118
|
+
def do_POST(self) -> None:
|
|
119
|
+
length = int(self.headers.get("Content-Length", 0))
|
|
120
|
+
if length > MAX_BODY:
|
|
121
|
+
self.send_error(413, "Request too large")
|
|
122
|
+
return
|
|
123
|
+
try:
|
|
124
|
+
body = json.loads(self.rfile.read(length)) if length else {}
|
|
125
|
+
except (json.JSONDecodeError, ValueError):
|
|
126
|
+
self.send_error(400, "Invalid JSON")
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
if self.path == "/events":
|
|
130
|
+
events = body.get("events", [])
|
|
131
|
+
accepted = 0
|
|
132
|
+
for e in events:
|
|
133
|
+
etype = e.get("type", "unknown")
|
|
134
|
+
meta = e.get("metadata", {})
|
|
135
|
+
if "duration_ms" in e:
|
|
136
|
+
meta["duration_ms"] = e["duration_ms"]
|
|
137
|
+
base.emit(etype, meta)
|
|
138
|
+
accepted += 1
|
|
139
|
+
self._json({"accepted": accepted})
|
|
140
|
+
elif self.path == "/pair/register":
|
|
141
|
+
token = body.get("token", "")
|
|
142
|
+
if not token or not body.get("session_id"):
|
|
143
|
+
self.send_error(400, "Missing token or session_id")
|
|
144
|
+
return
|
|
145
|
+
_pairing[token] = {
|
|
146
|
+
"session_id": body["session_id"],
|
|
147
|
+
"api_token": body.get("api_token", ""),
|
|
148
|
+
"api_base": body.get("api_base", ""),
|
|
149
|
+
"e2e_key": body.get("e2e_key", ""),
|
|
150
|
+
"paired": False,
|
|
151
|
+
}
|
|
152
|
+
self._json({"ok": True, "token": token})
|
|
153
|
+
elif self.path == "/pair/ack":
|
|
154
|
+
token = body.get("token", "")
|
|
155
|
+
data = _pairing.get(token)
|
|
156
|
+
if data:
|
|
157
|
+
data["paired"] = True
|
|
158
|
+
_extension_paired.set()
|
|
159
|
+
base.log("info", "extension.paired", session_id=data["session_id"])
|
|
160
|
+
self._json({"ok": bool(data)})
|
|
161
|
+
else:
|
|
162
|
+
self.send_error(404)
|
|
163
|
+
|
|
164
|
+
def do_OPTIONS(self) -> None:
|
|
165
|
+
self.send_response(204)
|
|
166
|
+
self._cors()
|
|
167
|
+
self.end_headers()
|
|
168
|
+
|
|
169
|
+
def _json(self, data: Any) -> None:
|
|
170
|
+
body = json.dumps(data).encode()
|
|
171
|
+
self.send_response(200)
|
|
172
|
+
self.send_header("Content-Type", "application/json")
|
|
173
|
+
self._cors()
|
|
174
|
+
self.end_headers()
|
|
175
|
+
self.wfile.write(body)
|
|
176
|
+
|
|
177
|
+
def _cors(self) -> None:
|
|
178
|
+
origin = self.headers.get("Origin", "")
|
|
179
|
+
allowed = origin.startswith("chrome-extension://") or origin.startswith("http://localhost")
|
|
180
|
+
self.send_header("Access-Control-Allow-Origin", origin if allowed else "http://localhost:9877")
|
|
181
|
+
self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
|
182
|
+
self.send_header("Access-Control-Allow-Headers", "Content-Type")
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def start(session_id: str, stop: threading.Event, port: int = 9877,
|
|
186
|
+
api_token: str = "", api_base: str = "", e2e_key: str = "") -> None:
|
|
187
|
+
global _session_id, _api_token, _api_base, _e2e_key
|
|
188
|
+
_session_id = session_id
|
|
189
|
+
_api_token = api_token
|
|
190
|
+
_api_base = api_base
|
|
191
|
+
_e2e_key = e2e_key
|
|
192
|
+
_extension_paired.clear()
|
|
193
|
+
HTTPServer.allow_reuse_address = True
|
|
194
|
+
server = HTTPServer(("127.0.0.1", port), _Handler)
|
|
195
|
+
server.timeout = 1
|
|
196
|
+
base.log("info", "bridge.started", port=port)
|
|
197
|
+
while not stop.is_set():
|
|
198
|
+
server.handle_request()
|
|
199
|
+
server.server_close()
|
|
200
|
+
base.log("info", "bridge.stopped")
|