methodproof 0.7.12__tar.gz → 0.7.14__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.7.14/.code-review-graph/.gitignore +3 -0
- methodproof-0.7.14/.code-review-graph/graph.db +0 -0
- {methodproof-0.7.12 → methodproof-0.7.14}/PKG-INFO +4 -1
- {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/__init__.py +1 -1
- {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/cli.py +136 -7
- {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/config.py +1 -0
- {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/sync.py +7 -5
- methodproof-0.7.14/methodproof/tui/__init__.py +1 -0
- methodproof-0.7.14/methodproof/tui/consent.py +256 -0
- methodproof-0.7.14/methodproof/tui/log.py +155 -0
- methodproof-0.7.14/methodproof/tui/review.py +137 -0
- methodproof-0.7.14/methodproof/tui/start.py +200 -0
- methodproof-0.7.14/methodproof/tui/status.py +154 -0
- methodproof-0.7.14/methodproof/tui/theme.py +53 -0
- {methodproof-0.7.12 → methodproof-0.7.14}/pyproject.toml +2 -1
- {methodproof-0.7.12 → methodproof-0.7.14}/.github/workflows/ci.yml +0 -0
- {methodproof-0.7.12 → methodproof-0.7.14}/.gitignore +0 -0
- {methodproof-0.7.12 → methodproof-0.7.14}/CHANGELOG.md +0 -0
- {methodproof-0.7.12 → methodproof-0.7.14}/LICENSE +0 -0
- {methodproof-0.7.12 → methodproof-0.7.14}/README.md +0 -0
- {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/__main__.py +0 -0
- {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/_daemon.py +0 -0
- {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/agents/__init__.py +0 -0
- {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/agents/base.py +0 -0
- {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/agents/music.py +0 -0
- {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/agents/terminal.py +0 -0
- {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/agents/watcher.py +0 -0
- {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/analysis.py +0 -0
- {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/binding.py +0 -0
- {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/bip39.py +0 -0
- {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/bridge.py +0 -0
- {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/crypto.py +0 -0
- {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/e2e.py +0 -0
- {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/graph.py +0 -0
- {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/hook.py +0 -0
- {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/hooks/__init__.py +0 -0
- {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/hooks/claude_code.py +0 -0
- {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/hooks/claude_code.sh +0 -0
- {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/hooks/cline_hook.sh +0 -0
- {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/hooks/codex_hook.sh +0 -0
- {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/hooks/gemini_hook.sh +0 -0
- {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/hooks/install.py +0 -0
- {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/hooks/kiro_hook.sh +0 -0
- {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/hooks/mcp_register.py +0 -0
- {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/hooks/openclaw/HOOK.md +0 -0
- {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/hooks/openclaw/handler.ts +0 -0
- {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/hooks/openclaw_install.py +0 -0
- {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/hooks/opencode_plugin.js +0 -0
- {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/hooks/wrappers.py +0 -0
- {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/integrity.py +0 -0
- {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/kdf.py +0 -0
- {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/keychain.py +0 -0
- {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/live.py +0 -0
- {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/lock.py +0 -0
- {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/mcp.py +0 -0
- {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/migrate_db.py +0 -0
- {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/proxy.py +0 -0
- {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/proxy_daemon.py +0 -0
- {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/repos.py +0 -0
- {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/skills/methodproof/SKILL.md +0 -0
- {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/store.py +0 -0
- {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/viewer.py +0 -0
- {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/wordlist.py +0 -0
- {methodproof-0.7.12 → methodproof-0.7.14}/test_windows_compat.py +0 -0
- {methodproof-0.7.12 → methodproof-0.7.14}/tests/__init__.py +0 -0
- {methodproof-0.7.12 → methodproof-0.7.14}/tests/conftest.py +0 -0
- {methodproof-0.7.12 → methodproof-0.7.14}/tests/test_analysis.py +0 -0
- {methodproof-0.7.12 → methodproof-0.7.14}/tests/test_cli_auth.py +0 -0
- {methodproof-0.7.12 → methodproof-0.7.14}/tests/test_cli_config.py +0 -0
- {methodproof-0.7.12 → methodproof-0.7.14}/tests/test_cli_helpers.py +0 -0
- {methodproof-0.7.12 → methodproof-0.7.14}/tests/test_cli_session.py +0 -0
- {methodproof-0.7.12 → methodproof-0.7.14}/tests/test_cli_share.py +0 -0
- {methodproof-0.7.12 → methodproof-0.7.14}/tests/test_cli_start.py +0 -0
- {methodproof-0.7.12 → methodproof-0.7.14}/tests/test_cli_update.py +0 -0
- {methodproof-0.7.12 → methodproof-0.7.14}/tests/test_e2e_integration.py +0 -0
- {methodproof-0.7.12 → methodproof-0.7.14}/tests/test_graph.py +0 -0
- {methodproof-0.7.12 → methodproof-0.7.14}/tests/test_hooks.py +0 -0
- {methodproof-0.7.12 → methodproof-0.7.14}/tests/test_live.py +0 -0
- {methodproof-0.7.12 → methodproof-0.7.14}/tests/test_openclaw_hooks.py +0 -0
- {methodproof-0.7.12 → methodproof-0.7.14}/tests/test_profiles.py +0 -0
- {methodproof-0.7.12 → methodproof-0.7.14}/tests/test_security.py +0 -0
- {methodproof-0.7.12 → methodproof-0.7.14}/tests/test_store.py +0 -0
- {methodproof-0.7.12 → methodproof-0.7.14}/tests/test_sync.py +0 -0
- {methodproof-0.7.12 → methodproof-0.7.14}/tests/test_viewer.py +0 -0
- {methodproof-0.7.12 → methodproof-0.7.14}/tests/test_wrappers.py +0 -0
- {methodproof-0.7.12 → methodproof-0.7.14}/uv.lock +0 -0
|
Binary file
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: methodproof
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.14
|
|
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
|
|
@@ -11,6 +11,9 @@ Requires-Dist: watchdog>=4.0
|
|
|
11
11
|
Requires-Dist: websocket-client>=1.7
|
|
12
12
|
Provides-Extra: proxy
|
|
13
13
|
Requires-Dist: mitmproxy>=10.0; extra == 'proxy'
|
|
14
|
+
Provides-Extra: ui
|
|
15
|
+
Requires-Dist: rich>=13.7; extra == 'ui'
|
|
16
|
+
Requires-Dist: textual>=0.59; extra == 'ui'
|
|
14
17
|
Description-Content-Type: text/markdown
|
|
15
18
|
|
|
16
19
|
<p align="center">
|
|
@@ -335,7 +335,7 @@ def cmd_init(args: argparse.Namespace) -> None:
|
|
|
335
335
|
config.ensure_dirs()
|
|
336
336
|
cfg = config.load()
|
|
337
337
|
if getattr(args, "force", False):
|
|
338
|
-
for key in ("consent_acknowledged", "auto_update_offered", "alias_offered", "local_ai_ports_offered"):
|
|
338
|
+
for key in ("consent_acknowledged", "auto_update_offered", "alias_offered", "local_ai_ports_offered", "ui_mode_offered"):
|
|
339
339
|
cfg.pop(key, None)
|
|
340
340
|
config.save(cfg)
|
|
341
341
|
|
|
@@ -372,6 +372,18 @@ def cmd_init(args: argparse.Namespace) -> None:
|
|
|
372
372
|
print("Alias: skipped")
|
|
373
373
|
config.save(cfg)
|
|
374
374
|
|
|
375
|
+
# Offer classic CLI mode (TUI is default)
|
|
376
|
+
if not cfg.get("ui_mode_offered"):
|
|
377
|
+
cfg["ui_mode_offered"] = True
|
|
378
|
+
answer = input("Prefer classic terminal output instead of the rich TUI? [y/N]: ").strip().lower()
|
|
379
|
+
if answer == "y":
|
|
380
|
+
cfg["ui_mode"] = False
|
|
381
|
+
print("Output mode: classic (toggle anytime with: mp ui on/off)")
|
|
382
|
+
else:
|
|
383
|
+
cfg["ui_mode"] = True
|
|
384
|
+
print("Output mode: rich TUI (toggle anytime with: mp ui off)")
|
|
385
|
+
config.save(cfg)
|
|
386
|
+
|
|
375
387
|
# Shell hook — needed for terminal commands
|
|
376
388
|
if capture.get("terminal_commands", True):
|
|
377
389
|
rc = hook.install()
|
|
@@ -458,6 +470,58 @@ def cmd_shell_hook(_args: argparse.Namespace) -> None:
|
|
|
458
470
|
print(hook_text.strip())
|
|
459
471
|
|
|
460
472
|
|
|
473
|
+
# ── TUI mode helpers ──────────────────────────────────────────────────────────
|
|
474
|
+
|
|
475
|
+
def _add_ui_flags(parser: argparse.ArgumentParser) -> None:
|
|
476
|
+
"""Add --ui / --no-ui flags to a subparser."""
|
|
477
|
+
g = parser.add_mutually_exclusive_group()
|
|
478
|
+
g.add_argument("--ui", dest="ui", action="store_true", default=None, help="Force TUI output")
|
|
479
|
+
g.add_argument("--no-ui", dest="ui", action="store_false", help="Force classic output")
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def _tui_guard() -> None:
|
|
483
|
+
"""Raise SystemExit with install hint if textual is not installed."""
|
|
484
|
+
import importlib.util
|
|
485
|
+
if importlib.util.find_spec("textual") is None:
|
|
486
|
+
raise SystemExit(
|
|
487
|
+
"TUI mode requires the ui extras.\n"
|
|
488
|
+
"Install with: pip install methodproof[ui]\n"
|
|
489
|
+
"Or switch back: mp ui off"
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def _resolve_ui(args: argparse.Namespace, cfg: dict) -> bool:
|
|
494
|
+
"""Return True if TUI mode should be used for this invocation."""
|
|
495
|
+
flag = getattr(args, "ui", None) # True / False / None (not specified)
|
|
496
|
+
if flag is True:
|
|
497
|
+
return True
|
|
498
|
+
if flag is False:
|
|
499
|
+
return False
|
|
500
|
+
return cfg.get("ui_mode", True)
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def cmd_ui(args: argparse.Namespace) -> None:
|
|
504
|
+
"""Toggle or report TUI mode."""
|
|
505
|
+
import importlib.util
|
|
506
|
+
cfg = config.load()
|
|
507
|
+
sub = getattr(args, "ui_cmd", None)
|
|
508
|
+
if sub == "on":
|
|
509
|
+
cfg["ui_mode"] = True
|
|
510
|
+
config.save(cfg)
|
|
511
|
+
print("TUI mode: on (mp consent, mp log, mp start, mp status, mp review)")
|
|
512
|
+
if importlib.util.find_spec("textual") is None:
|
|
513
|
+
print("Install libraries: pip install methodproof[ui]")
|
|
514
|
+
elif sub == "off":
|
|
515
|
+
cfg["ui_mode"] = False
|
|
516
|
+
config.save(cfg)
|
|
517
|
+
print("TUI mode: off (classic terminal output)")
|
|
518
|
+
else:
|
|
519
|
+
mode = cfg.get("ui_mode", True)
|
|
520
|
+
installed = importlib.util.find_spec("textual") is not None
|
|
521
|
+
print(f"TUI mode: {'on' if mode else 'off'}")
|
|
522
|
+
print(f"Libraries: {'installed ✓' if installed else 'not installed (pip install methodproof[ui])'}")
|
|
523
|
+
|
|
524
|
+
|
|
461
525
|
def _print_commands() -> None:
|
|
462
526
|
"""Print color coded command reference."""
|
|
463
527
|
if not sys.stdout.isatty():
|
|
@@ -738,6 +802,18 @@ def cmd_reset(args: argparse.Namespace) -> None:
|
|
|
738
802
|
def cmd_consent(args: argparse.Namespace) -> None:
|
|
739
803
|
"""Review or change capture, research, and redaction settings."""
|
|
740
804
|
cfg = config.load()
|
|
805
|
+
if _resolve_ui(args, cfg):
|
|
806
|
+
_tui_guard()
|
|
807
|
+
from methodproof.tui.consent import run as tui_consent
|
|
808
|
+
cfg = tui_consent(cfg)
|
|
809
|
+
config.save(cfg)
|
|
810
|
+
if cfg.get("token"):
|
|
811
|
+
cfg["_pending_research_sync"] = True
|
|
812
|
+
config.save(cfg)
|
|
813
|
+
from methodproof.sync import sync_research_consent
|
|
814
|
+
sync_research_consent(cfg["token"], cfg["api_url"])
|
|
815
|
+
return
|
|
816
|
+
# Classic flow
|
|
741
817
|
print(f"\n{_banner()}\n")
|
|
742
818
|
cfg = _run_consent_detailed(cfg)
|
|
743
819
|
config.save(cfg)
|
|
@@ -1154,6 +1230,15 @@ def cmd_start(args: argparse.Namespace) -> None:
|
|
|
1154
1230
|
print("Extension: not detected — run `mp extension pair` or install from store")
|
|
1155
1231
|
except Exception as exc:
|
|
1156
1232
|
print(f"Extension: not detected ({exc})")
|
|
1233
|
+
if _resolve_ui(args, cfg):
|
|
1234
|
+
try:
|
|
1235
|
+
_tui_guard()
|
|
1236
|
+
session = store.get_session(sid)
|
|
1237
|
+
from methodproof.tui.start import run as tui_start
|
|
1238
|
+
tui_start(sid, session)
|
|
1239
|
+
return
|
|
1240
|
+
except SystemExit:
|
|
1241
|
+
pass # textual not installed — fall through to plain message
|
|
1157
1242
|
print("Run `mp stop` to finish.")
|
|
1158
1243
|
return
|
|
1159
1244
|
|
|
@@ -1251,8 +1336,16 @@ def cmd_stop(args: argparse.Namespace) -> None:
|
|
|
1251
1336
|
cfg = config.load()
|
|
1252
1337
|
sid = cfg.get("active_session")
|
|
1253
1338
|
if not sid:
|
|
1254
|
-
|
|
1255
|
-
|
|
1339
|
+
# Config lost track — check store for dangling sessions on this directory
|
|
1340
|
+
watch_dir = os.path.abspath(".")
|
|
1341
|
+
dangling = store.find_active_for_dir(watch_dir)
|
|
1342
|
+
if dangling:
|
|
1343
|
+
sid = dangling["id"]
|
|
1344
|
+
cfg["active_session"] = sid
|
|
1345
|
+
config.save(cfg)
|
|
1346
|
+
else:
|
|
1347
|
+
print("No active session.")
|
|
1348
|
+
sys.exit(1)
|
|
1256
1349
|
|
|
1257
1350
|
# Signal the start process if it's running
|
|
1258
1351
|
if PIDFILE.exists():
|
|
@@ -1294,6 +1387,20 @@ def cmd_view(args: argparse.Namespace) -> None:
|
|
|
1294
1387
|
|
|
1295
1388
|
|
|
1296
1389
|
def cmd_log(args: argparse.Namespace) -> None:
|
|
1390
|
+
cfg = config.load()
|
|
1391
|
+
if _resolve_ui(args, cfg):
|
|
1392
|
+
_tui_guard()
|
|
1393
|
+
from methodproof.tui.log import run as tui_log
|
|
1394
|
+
result = tui_log()
|
|
1395
|
+
if result:
|
|
1396
|
+
action, sid = result
|
|
1397
|
+
import argparse as _ap
|
|
1398
|
+
fake = _ap.Namespace(session_id=sid, local=False)
|
|
1399
|
+
if action == "push":
|
|
1400
|
+
cmd_push(fake)
|
|
1401
|
+
elif action == "view":
|
|
1402
|
+
cmd_view(fake)
|
|
1403
|
+
return
|
|
1297
1404
|
sessions = store.list_sessions()
|
|
1298
1405
|
if not sessions:
|
|
1299
1406
|
print("No sessions yet.")
|
|
@@ -1318,8 +1425,13 @@ def cmd_log(args: argparse.Namespace) -> None:
|
|
|
1318
1425
|
|
|
1319
1426
|
def cmd_status(args: argparse.Namespace) -> None:
|
|
1320
1427
|
"""Show auth, session, and config status at a glance."""
|
|
1321
|
-
from methodproof import __version__
|
|
1322
1428
|
cfg = config.load()
|
|
1429
|
+
if _resolve_ui(args, cfg):
|
|
1430
|
+
_tui_guard()
|
|
1431
|
+
from methodproof.tui.status import run as tui_status
|
|
1432
|
+
tui_status(cfg)
|
|
1433
|
+
return
|
|
1434
|
+
from methodproof import __version__
|
|
1323
1435
|
token = cfg.get("token", "")
|
|
1324
1436
|
claims = _decode_jwt_claims(token) if token else {}
|
|
1325
1437
|
sessions = store.list_sessions()
|
|
@@ -1663,7 +1775,13 @@ def cmd_delete(args: argparse.Namespace) -> None:
|
|
|
1663
1775
|
|
|
1664
1776
|
def cmd_review(args: argparse.Namespace) -> None:
|
|
1665
1777
|
"""Show exactly what a session contains before pushing."""
|
|
1778
|
+
cfg = config.load()
|
|
1666
1779
|
session = _resolve_session(args.session_id)
|
|
1780
|
+
if _resolve_ui(args, cfg):
|
|
1781
|
+
_tui_guard()
|
|
1782
|
+
from methodproof.tui.review import run as tui_review
|
|
1783
|
+
tui_review(session)
|
|
1784
|
+
return
|
|
1667
1785
|
events = store.get_events(session["id"])
|
|
1668
1786
|
if not events:
|
|
1669
1787
|
print("No events in this session.")
|
|
@@ -1979,11 +2097,14 @@ def main() -> None:
|
|
|
1979
2097
|
s.add_argument("--no-e2e", action="store_true", help="Disable E2E for this session (overrides config)")
|
|
1980
2098
|
s.add_argument("--verbose", "-v", action="store_true", help="Debug logging at each step (still daemonizes)")
|
|
1981
2099
|
s.add_argument("--streaming", action="store_true", help="Blocking foreground — stream every captured event to stdout")
|
|
2100
|
+
_add_ui_flags(s)
|
|
1982
2101
|
sub.add_parser("stop", help="Stop recording")
|
|
1983
2102
|
v = sub.add_parser("view", help="Inspect captured session data")
|
|
1984
2103
|
v.add_argument("session_id", nargs="?")
|
|
1985
|
-
sub.add_parser("log", help="List sessions")
|
|
1986
|
-
|
|
2104
|
+
l_log = sub.add_parser("log", help="List sessions")
|
|
2105
|
+
_add_ui_flags(l_log)
|
|
2106
|
+
s_status = sub.add_parser("status", help="Auth, session, and config status")
|
|
2107
|
+
_add_ui_flags(s_status)
|
|
1987
2108
|
l = sub.add_parser("login", help="Connect to platform")
|
|
1988
2109
|
l.add_argument("--api-url")
|
|
1989
2110
|
l.add_argument("--force", "-f", action="store_true", help="Skip switch-account prompt")
|
|
@@ -2005,7 +2126,14 @@ def main() -> None:
|
|
|
2005
2126
|
dl.add_argument("--force", "-f", action="store_true", help="Skip confirmation")
|
|
2006
2127
|
rv = sub.add_parser("review", help="Review session data before pushing")
|
|
2007
2128
|
rv.add_argument("session_id", nargs="?")
|
|
2008
|
-
|
|
2129
|
+
_add_ui_flags(rv)
|
|
2130
|
+
c_consent = sub.add_parser("consent", help="Change capture, research, and redaction settings")
|
|
2131
|
+
_add_ui_flags(c_consent)
|
|
2132
|
+
ui_p = sub.add_parser("ui", help="Toggle TUI mode on/off")
|
|
2133
|
+
ui_sub = ui_p.add_subparsers(dest="ui_cmd")
|
|
2134
|
+
ui_sub.add_parser("on", help="Enable TUI mode")
|
|
2135
|
+
ui_sub.add_parser("off", help="Disable TUI mode (classic output)")
|
|
2136
|
+
ui_sub.add_parser("status", help="Show TUI mode and library status")
|
|
2009
2137
|
up = sub.add_parser("update", help="Update to the latest version from PyPI")
|
|
2010
2138
|
up_auto = up.add_mutually_exclusive_group()
|
|
2011
2139
|
up_auto.add_argument("--auto", dest="auto", action="store_true", default=None,
|
|
@@ -2062,6 +2190,7 @@ def main() -> None:
|
|
|
2062
2190
|
"intro": lambda _: _print_intro(),
|
|
2063
2191
|
"help": lambda _: _print_commands(),
|
|
2064
2192
|
"shell-hook": cmd_shell_hook,
|
|
2193
|
+
"ui": cmd_ui,
|
|
2065
2194
|
"mcp-serve": cmd_mcp_serve,
|
|
2066
2195
|
"proxy": lambda a: __import__("methodproof.proxy", fromlist=["cmd_proxy"]).cmd_proxy(a),
|
|
2067
2196
|
}
|
|
@@ -30,6 +30,7 @@ def sync_metadata(session: dict[str, Any], token: str, api_url: str) -> None:
|
|
|
30
30
|
def _raw_request(
|
|
31
31
|
method: str, url: str, token: str,
|
|
32
32
|
body: dict[str, Any] | None = None,
|
|
33
|
+
timeout: int = 15,
|
|
33
34
|
) -> dict[str, Any]:
|
|
34
35
|
if body is not None:
|
|
35
36
|
data = gzip.compress(json.dumps(body).encode())
|
|
@@ -39,7 +40,7 @@ def _raw_request(
|
|
|
39
40
|
if data is not None:
|
|
40
41
|
headers["Content-Encoding"] = "gzip"
|
|
41
42
|
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
|
42
|
-
with urllib.request.urlopen(req, timeout=
|
|
43
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
43
44
|
return json.loads(resp.read())
|
|
44
45
|
|
|
45
46
|
|
|
@@ -57,10 +58,11 @@ def _refresh_token(api_url: str, refresh: str) -> tuple[str, str] | None:
|
|
|
57
58
|
def _request(
|
|
58
59
|
method: str, path: str, api_url: str, token: str,
|
|
59
60
|
body: dict[str, Any] | None = None,
|
|
61
|
+
timeout: int = 15,
|
|
60
62
|
) -> dict[str, Any]:
|
|
61
63
|
url = f"{api_url}{path}"
|
|
62
64
|
try:
|
|
63
|
-
return _raw_request(method, url, token, body)
|
|
65
|
+
return _raw_request(method, url, token, body, timeout=timeout)
|
|
64
66
|
except urllib.error.HTTPError as exc:
|
|
65
67
|
if exc.code == 401:
|
|
66
68
|
from methodproof import config
|
|
@@ -71,7 +73,7 @@ def _request(
|
|
|
71
73
|
if pair:
|
|
72
74
|
cfg["token"], cfg["refresh_token"] = pair
|
|
73
75
|
config.save(cfg)
|
|
74
|
-
return _raw_request(method, url, cfg["token"], body)
|
|
76
|
+
return _raw_request(method, url, cfg["token"], body, timeout=timeout)
|
|
75
77
|
raise SystemExit("Session expired. Run `methodproof login` to re-authenticate.") from None
|
|
76
78
|
detail = ""
|
|
77
79
|
if exc.fp:
|
|
@@ -170,8 +172,8 @@ def push(session_id: str, token: str, api_url: str) -> str:
|
|
|
170
172
|
except ImportError:
|
|
171
173
|
pass # cryptography not installed
|
|
172
174
|
|
|
173
|
-
# Complete
|
|
174
|
-
_request("PUT", f"/personal/sessions/{remote_id}/complete", api_url, token)
|
|
175
|
+
# Complete (server drains ingest queue + materializes stats — needs longer timeout)
|
|
176
|
+
_request("PUT", f"/personal/sessions/{remote_id}/complete", api_url, token, timeout=90)
|
|
175
177
|
store.mark_synced(session_id, remote_id)
|
|
176
178
|
|
|
177
179
|
# Sync metadata (repo, tags, visibility)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# methodproof TUI — requires `pip install methodproof[ui]`
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"""Textual TUI for mp consent — capture categories + publish redaction."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from textual.app import App, ComposeResult
|
|
5
|
+
from textual.binding import Binding
|
|
6
|
+
from textual.containers import Horizontal, ScrollableContainer, Vertical
|
|
7
|
+
from textual.widgets import Footer, Header, Label, Rule, Static, Switch
|
|
8
|
+
|
|
9
|
+
from methodproof import config as cfg_mod
|
|
10
|
+
from methodproof.tui.theme import BASE_CSS, BORDER, DIM, GOLD, GREEN, PURPLE, RED, TEXT
|
|
11
|
+
|
|
12
|
+
# Redaction settings shown in Section B
|
|
13
|
+
_REDACTABLE = [
|
|
14
|
+
("command_output", "Terminal output (first 500 chars)"),
|
|
15
|
+
("ai_prompts", "AI prompt text"),
|
|
16
|
+
("ai_responses", "AI response text"),
|
|
17
|
+
("code_capture", "File diffs and git patches"),
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
# Which sample field each publish_redact key covers (for live preview)
|
|
21
|
+
_REDACT_FIELD: dict[str, str] = {
|
|
22
|
+
"command_output": "output_snippet",
|
|
23
|
+
"ai_prompts": "prompt_text",
|
|
24
|
+
"ai_responses": "response_text",
|
|
25
|
+
"code_capture": "diff",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
_SAMPLE_EVENT = {
|
|
29
|
+
"type": "terminal_cmd",
|
|
30
|
+
"command": "pytest tests/ -v",
|
|
31
|
+
"exit_code": 0,
|
|
32
|
+
"duration_ms": 4201,
|
|
33
|
+
"output_snippet": "12 passed in 4.2s",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
_CSS = BASE_CSS + f"""
|
|
37
|
+
#main {{
|
|
38
|
+
padding: 1 3;
|
|
39
|
+
}}
|
|
40
|
+
#heading {{
|
|
41
|
+
color: {TEXT};
|
|
42
|
+
text-style: bold;
|
|
43
|
+
margin: 0 0 0 0;
|
|
44
|
+
}}
|
|
45
|
+
#subhead {{
|
|
46
|
+
color: {DIM};
|
|
47
|
+
margin: 0 0 1 0;
|
|
48
|
+
}}
|
|
49
|
+
.toggle-row {{
|
|
50
|
+
height: 2;
|
|
51
|
+
align: left middle;
|
|
52
|
+
margin: 0;
|
|
53
|
+
}}
|
|
54
|
+
.toggle-row Switch {{
|
|
55
|
+
margin: 0 1 0 0;
|
|
56
|
+
width: 4;
|
|
57
|
+
}}
|
|
58
|
+
.pro-row .row-label {{
|
|
59
|
+
color: {PURPLE};
|
|
60
|
+
}}
|
|
61
|
+
.pro-badge {{
|
|
62
|
+
background: #200a26;
|
|
63
|
+
color: {PURPLE};
|
|
64
|
+
padding: 0 1;
|
|
65
|
+
margin: 0 0 0 2;
|
|
66
|
+
width: 5;
|
|
67
|
+
}}
|
|
68
|
+
#fs-status {{
|
|
69
|
+
color: {DIM};
|
|
70
|
+
margin: 1 0 0 0;
|
|
71
|
+
height: 1;
|
|
72
|
+
}}
|
|
73
|
+
#fs-status.full {{
|
|
74
|
+
color: {GOLD};
|
|
75
|
+
}}
|
|
76
|
+
#preview-box {{
|
|
77
|
+
background: #050403;
|
|
78
|
+
border: solid {BORDER};
|
|
79
|
+
margin: 1 0 0 0;
|
|
80
|
+
padding: 1 2;
|
|
81
|
+
height: auto;
|
|
82
|
+
}}
|
|
83
|
+
.preview-header {{
|
|
84
|
+
color: {DIM};
|
|
85
|
+
margin: 0 0 1 0;
|
|
86
|
+
}}
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _build_preview(redact: dict) -> str:
|
|
91
|
+
"""Rich markup string showing the sample event with redacted fields highlighted."""
|
|
92
|
+
lines = [f"[{DIM}]{{[/{DIM}]"]
|
|
93
|
+
sample = {**_SAMPLE_EVENT}
|
|
94
|
+
|
|
95
|
+
# Mark fields that would be stripped on publish
|
|
96
|
+
redacted_fields = {
|
|
97
|
+
field for key, field in _REDACT_FIELD.items() if redact.get(key, True)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
for k, v in sample.items():
|
|
101
|
+
if k in redacted_fields:
|
|
102
|
+
lines.append(f' [{RED}]"{k}"[/{RED}]: [{RED} dim]\\[redacted][/{RED} dim],')
|
|
103
|
+
elif isinstance(v, str):
|
|
104
|
+
lines.append(f' [{GOLD}]"{k}"[/{GOLD}]: [{GREEN}]"{v}"[/{GREEN}],')
|
|
105
|
+
else:
|
|
106
|
+
lines.append(f' [{GOLD}]"{k}"[/{GOLD}]: [{TEXT}]{v}[/{TEXT}],')
|
|
107
|
+
|
|
108
|
+
lines.append(f"[{DIM}]}}[/{DIM}]")
|
|
109
|
+
return "\n".join(lines)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class ConsentApp(App[dict | None]):
|
|
113
|
+
"""Consent + Redaction TUI. Exits with updated cfg dict on save, None on cancel."""
|
|
114
|
+
|
|
115
|
+
TITLE = "methodproof — mp consent"
|
|
116
|
+
CSS = _CSS
|
|
117
|
+
BINDINGS = [
|
|
118
|
+
Binding("s", "save", "save"),
|
|
119
|
+
Binding("escape", "cancel", "cancel"),
|
|
120
|
+
Binding("r", "reset_defaults", "reset"),
|
|
121
|
+
]
|
|
122
|
+
|
|
123
|
+
def __init__(self, cfg: dict) -> None:
|
|
124
|
+
super().__init__()
|
|
125
|
+
self._cfg = cfg
|
|
126
|
+
self._capture = dict(cfg.get("capture", cfg_mod._DEFAULTS["capture"]))
|
|
127
|
+
self._redact = dict(cfg.get("publish_redact", cfg_mod._DEFAULTS["publish_redact"]))
|
|
128
|
+
|
|
129
|
+
def compose(self) -> ComposeResult:
|
|
130
|
+
yield Header(show_clock=False)
|
|
131
|
+
with ScrollableContainer(id="main"):
|
|
132
|
+
yield Static("Configure Data Capture", id="heading")
|
|
133
|
+
yield Static(
|
|
134
|
+
"Control exactly what leaves your machine. Nothing uploads until mp push.",
|
|
135
|
+
id="subhead",
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# ── Section A: Capture categories ────────────────────
|
|
139
|
+
with Vertical(classes="panel", id="capture-panel"):
|
|
140
|
+
yield Static(" Structural Capture ", classes="section-title")
|
|
141
|
+
|
|
142
|
+
for cat in cfg_mod.STANDARD_CATEGORIES:
|
|
143
|
+
label = cat.replace("_", " ")
|
|
144
|
+
desc = cfg_mod.CAPTURE_DESCRIPTIONS[cat]
|
|
145
|
+
with Horizontal(classes="toggle-row"):
|
|
146
|
+
yield Switch(
|
|
147
|
+
value=self._capture.get(cat, True),
|
|
148
|
+
id=f"cap-{cat}",
|
|
149
|
+
animate=False,
|
|
150
|
+
)
|
|
151
|
+
yield Label(label, classes="row-label")
|
|
152
|
+
yield Label(desc, classes="row-desc")
|
|
153
|
+
|
|
154
|
+
yield Static("", id="fs-status")
|
|
155
|
+
yield Rule()
|
|
156
|
+
|
|
157
|
+
# Code capture — Pro, off by default
|
|
158
|
+
with Horizontal(classes="toggle-row pro-row"):
|
|
159
|
+
yield Switch(
|
|
160
|
+
value=self._capture.get("code_capture", False),
|
|
161
|
+
id="cap-code_capture",
|
|
162
|
+
animate=False,
|
|
163
|
+
)
|
|
164
|
+
yield Label("code capture", classes="row-label")
|
|
165
|
+
yield Label(cfg_mod.CAPTURE_DESCRIPTIONS["code_capture"], classes="row-desc")
|
|
166
|
+
yield Static("Pro", classes="pro-badge")
|
|
167
|
+
|
|
168
|
+
# ── Section B: Publish redaction ─────────────────────
|
|
169
|
+
with Vertical(classes="panel", id="redact-panel"):
|
|
170
|
+
yield Static(" Publish Redaction ", classes="section-title")
|
|
171
|
+
yield Static(
|
|
172
|
+
"Stripped when you publish a session publicly (mp publish).",
|
|
173
|
+
classes="row-desc",
|
|
174
|
+
)
|
|
175
|
+
yield Static("", classes="row-desc") # spacer
|
|
176
|
+
|
|
177
|
+
for key, desc in _REDACTABLE:
|
|
178
|
+
with Horizontal(classes="toggle-row"):
|
|
179
|
+
yield Switch(
|
|
180
|
+
value=self._redact.get(key, True),
|
|
181
|
+
id=f"red-{key}",
|
|
182
|
+
animate=False,
|
|
183
|
+
)
|
|
184
|
+
yield Label(desc, classes="row-label")
|
|
185
|
+
|
|
186
|
+
# Live redaction preview
|
|
187
|
+
with Vertical(id="preview-box"):
|
|
188
|
+
yield Static(
|
|
189
|
+
"What leaves your machine on mp push with current settings:",
|
|
190
|
+
classes="preview-header",
|
|
191
|
+
)
|
|
192
|
+
yield Static(
|
|
193
|
+
_build_preview(self._redact),
|
|
194
|
+
id="preview-content",
|
|
195
|
+
markup=True,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
yield Footer()
|
|
199
|
+
|
|
200
|
+
def on_mount(self) -> None:
|
|
201
|
+
self._refresh_fs_status()
|
|
202
|
+
|
|
203
|
+
def on_switch_changed(self, event: Switch.Changed) -> None:
|
|
204
|
+
sw_id = event.switch.id or ""
|
|
205
|
+
if sw_id.startswith("cap-"):
|
|
206
|
+
self._capture[sw_id[4:]] = event.value
|
|
207
|
+
self._refresh_fs_status()
|
|
208
|
+
elif sw_id.startswith("red-"):
|
|
209
|
+
self._redact[sw_id[4:]] = event.value
|
|
210
|
+
self._refresh_preview()
|
|
211
|
+
|
|
212
|
+
def _refresh_fs_status(self) -> None:
|
|
213
|
+
enabled = sum(1 for k in cfg_mod.STANDARD_CATEGORIES if self._capture.get(k, True))
|
|
214
|
+
is_full = enabled == len(cfg_mod.STANDARD_CATEGORIES)
|
|
215
|
+
widget = self.query_one("#fs-status", Static)
|
|
216
|
+
if is_full:
|
|
217
|
+
widget.update("★ Full Spectrum — free live streaming unlocked")
|
|
218
|
+
widget.remove_class("full")
|
|
219
|
+
widget.add_class("full")
|
|
220
|
+
else:
|
|
221
|
+
widget.update(
|
|
222
|
+
f" {enabled} / {len(cfg_mod.STANDARD_CATEGORIES)} enabled"
|
|
223
|
+
" — enable all 10 for free live streaming"
|
|
224
|
+
)
|
|
225
|
+
widget.remove_class("full")
|
|
226
|
+
self._refresh_preview()
|
|
227
|
+
|
|
228
|
+
def _refresh_preview(self) -> None:
|
|
229
|
+
self.query_one("#preview-content", Static).update(
|
|
230
|
+
_build_preview(self._redact)
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
def action_save(self) -> None:
|
|
234
|
+
self._cfg["capture"] = self._capture
|
|
235
|
+
self._cfg["publish_redact"] = self._redact
|
|
236
|
+
self._cfg["consent_acknowledged"] = True
|
|
237
|
+
self.exit(self._cfg)
|
|
238
|
+
|
|
239
|
+
def action_cancel(self) -> None:
|
|
240
|
+
self.exit(None)
|
|
241
|
+
|
|
242
|
+
def action_reset_defaults(self) -> None:
|
|
243
|
+
self._capture = dict(cfg_mod._DEFAULTS["capture"])
|
|
244
|
+
self._redact = dict(cfg_mod._DEFAULTS["publish_redact"])
|
|
245
|
+
for cat in cfg_mod.STANDARD_CATEGORIES:
|
|
246
|
+
self.query_one(f"#cap-{cat}", Switch).value = self._capture[cat]
|
|
247
|
+
self.query_one("#cap-code_capture", Switch).value = self._capture["code_capture"]
|
|
248
|
+
for key, _ in _REDACTABLE:
|
|
249
|
+
self.query_one(f"#red-{key}", Switch).value = self._redact[key]
|
|
250
|
+
self._refresh_fs_status()
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def run(cfg: dict) -> dict:
|
|
254
|
+
"""Launch the consent TUI. Returns updated cfg, or unchanged cfg if cancelled."""
|
|
255
|
+
result = ConsentApp(cfg).run()
|
|
256
|
+
return result if result is not None else cfg
|