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.
Files changed (86) hide show
  1. methodproof-0.7.14/.code-review-graph/.gitignore +3 -0
  2. methodproof-0.7.14/.code-review-graph/graph.db +0 -0
  3. {methodproof-0.7.12 → methodproof-0.7.14}/PKG-INFO +4 -1
  4. {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/__init__.py +1 -1
  5. {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/cli.py +136 -7
  6. {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/config.py +1 -0
  7. {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/sync.py +7 -5
  8. methodproof-0.7.14/methodproof/tui/__init__.py +1 -0
  9. methodproof-0.7.14/methodproof/tui/consent.py +256 -0
  10. methodproof-0.7.14/methodproof/tui/log.py +155 -0
  11. methodproof-0.7.14/methodproof/tui/review.py +137 -0
  12. methodproof-0.7.14/methodproof/tui/start.py +200 -0
  13. methodproof-0.7.14/methodproof/tui/status.py +154 -0
  14. methodproof-0.7.14/methodproof/tui/theme.py +53 -0
  15. {methodproof-0.7.12 → methodproof-0.7.14}/pyproject.toml +2 -1
  16. {methodproof-0.7.12 → methodproof-0.7.14}/.github/workflows/ci.yml +0 -0
  17. {methodproof-0.7.12 → methodproof-0.7.14}/.gitignore +0 -0
  18. {methodproof-0.7.12 → methodproof-0.7.14}/CHANGELOG.md +0 -0
  19. {methodproof-0.7.12 → methodproof-0.7.14}/LICENSE +0 -0
  20. {methodproof-0.7.12 → methodproof-0.7.14}/README.md +0 -0
  21. {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/__main__.py +0 -0
  22. {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/_daemon.py +0 -0
  23. {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/agents/__init__.py +0 -0
  24. {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/agents/base.py +0 -0
  25. {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/agents/music.py +0 -0
  26. {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/agents/terminal.py +0 -0
  27. {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/agents/watcher.py +0 -0
  28. {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/analysis.py +0 -0
  29. {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/binding.py +0 -0
  30. {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/bip39.py +0 -0
  31. {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/bridge.py +0 -0
  32. {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/crypto.py +0 -0
  33. {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/e2e.py +0 -0
  34. {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/graph.py +0 -0
  35. {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/hook.py +0 -0
  36. {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/hooks/__init__.py +0 -0
  37. {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/hooks/claude_code.py +0 -0
  38. {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/hooks/claude_code.sh +0 -0
  39. {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/hooks/cline_hook.sh +0 -0
  40. {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/hooks/codex_hook.sh +0 -0
  41. {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/hooks/gemini_hook.sh +0 -0
  42. {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/hooks/install.py +0 -0
  43. {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/hooks/kiro_hook.sh +0 -0
  44. {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/hooks/mcp_register.py +0 -0
  45. {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/hooks/openclaw/HOOK.md +0 -0
  46. {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/hooks/openclaw/handler.ts +0 -0
  47. {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/hooks/openclaw_install.py +0 -0
  48. {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/hooks/opencode_plugin.js +0 -0
  49. {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/hooks/wrappers.py +0 -0
  50. {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/integrity.py +0 -0
  51. {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/kdf.py +0 -0
  52. {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/keychain.py +0 -0
  53. {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/live.py +0 -0
  54. {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/lock.py +0 -0
  55. {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/mcp.py +0 -0
  56. {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/migrate_db.py +0 -0
  57. {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/proxy.py +0 -0
  58. {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/proxy_daemon.py +0 -0
  59. {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/repos.py +0 -0
  60. {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/skills/methodproof/SKILL.md +0 -0
  61. {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/store.py +0 -0
  62. {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/viewer.py +0 -0
  63. {methodproof-0.7.12 → methodproof-0.7.14}/methodproof/wordlist.py +0 -0
  64. {methodproof-0.7.12 → methodproof-0.7.14}/test_windows_compat.py +0 -0
  65. {methodproof-0.7.12 → methodproof-0.7.14}/tests/__init__.py +0 -0
  66. {methodproof-0.7.12 → methodproof-0.7.14}/tests/conftest.py +0 -0
  67. {methodproof-0.7.12 → methodproof-0.7.14}/tests/test_analysis.py +0 -0
  68. {methodproof-0.7.12 → methodproof-0.7.14}/tests/test_cli_auth.py +0 -0
  69. {methodproof-0.7.12 → methodproof-0.7.14}/tests/test_cli_config.py +0 -0
  70. {methodproof-0.7.12 → methodproof-0.7.14}/tests/test_cli_helpers.py +0 -0
  71. {methodproof-0.7.12 → methodproof-0.7.14}/tests/test_cli_session.py +0 -0
  72. {methodproof-0.7.12 → methodproof-0.7.14}/tests/test_cli_share.py +0 -0
  73. {methodproof-0.7.12 → methodproof-0.7.14}/tests/test_cli_start.py +0 -0
  74. {methodproof-0.7.12 → methodproof-0.7.14}/tests/test_cli_update.py +0 -0
  75. {methodproof-0.7.12 → methodproof-0.7.14}/tests/test_e2e_integration.py +0 -0
  76. {methodproof-0.7.12 → methodproof-0.7.14}/tests/test_graph.py +0 -0
  77. {methodproof-0.7.12 → methodproof-0.7.14}/tests/test_hooks.py +0 -0
  78. {methodproof-0.7.12 → methodproof-0.7.14}/tests/test_live.py +0 -0
  79. {methodproof-0.7.12 → methodproof-0.7.14}/tests/test_openclaw_hooks.py +0 -0
  80. {methodproof-0.7.12 → methodproof-0.7.14}/tests/test_profiles.py +0 -0
  81. {methodproof-0.7.12 → methodproof-0.7.14}/tests/test_security.py +0 -0
  82. {methodproof-0.7.12 → methodproof-0.7.14}/tests/test_store.py +0 -0
  83. {methodproof-0.7.12 → methodproof-0.7.14}/tests/test_sync.py +0 -0
  84. {methodproof-0.7.12 → methodproof-0.7.14}/tests/test_viewer.py +0 -0
  85. {methodproof-0.7.12 → methodproof-0.7.14}/tests/test_wrappers.py +0 -0
  86. {methodproof-0.7.12 → methodproof-0.7.14}/uv.lock +0 -0
@@ -0,0 +1,3 @@
1
+ # Auto-generated by code-review-graph — do not commit database files.
2
+ # The graph.db contains absolute paths and code structure metadata.
3
+ *
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: methodproof
3
- Version: 0.7.12
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">
@@ -1,3 +1,3 @@
1
1
  """MethodProof — see how you code."""
2
2
 
3
- __version__ = "0.7.8"
3
+ __version__ = "0.7.14"
@@ -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
- print("No active session.")
1255
- sys.exit(1)
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
- sub.add_parser("status", help="Auth, session, and config status")
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
- sub.add_parser("consent", help="Change capture, research, and redaction settings")
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
  }
@@ -51,6 +51,7 @@ _DEFAULTS: dict[str, Any] = {
51
51
  "code_capture": True,
52
52
  },
53
53
  "profiles": {},
54
+ "ui_mode": True,
54
55
  }
55
56
 
56
57
  FREE_JOURNAL_MAX_HOURS = 4
@@ -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=15) as resp:
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