methodproof 0.7.11__tar.gz → 0.7.13__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.13/.code-review-graph/.gitignore +3 -0
  2. methodproof-0.7.13/.code-review-graph/graph.db +0 -0
  3. {methodproof-0.7.11 → methodproof-0.7.13}/PKG-INFO +4 -1
  4. {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/__init__.py +1 -1
  5. {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/cli.py +128 -7
  6. {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/config.py +1 -0
  7. {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/hook.py +3 -2
  8. {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/sync.py +7 -5
  9. methodproof-0.7.13/methodproof/tui/__init__.py +1 -0
  10. methodproof-0.7.13/methodproof/tui/consent.py +256 -0
  11. methodproof-0.7.13/methodproof/tui/log.py +155 -0
  12. methodproof-0.7.13/methodproof/tui/review.py +137 -0
  13. methodproof-0.7.13/methodproof/tui/start.py +200 -0
  14. methodproof-0.7.13/methodproof/tui/status.py +154 -0
  15. methodproof-0.7.13/methodproof/tui/theme.py +53 -0
  16. {methodproof-0.7.11 → methodproof-0.7.13}/pyproject.toml +2 -1
  17. {methodproof-0.7.11 → methodproof-0.7.13}/uv.lock +1 -1
  18. {methodproof-0.7.11 → methodproof-0.7.13}/.github/workflows/ci.yml +0 -0
  19. {methodproof-0.7.11 → methodproof-0.7.13}/.gitignore +0 -0
  20. {methodproof-0.7.11 → methodproof-0.7.13}/CHANGELOG.md +0 -0
  21. {methodproof-0.7.11 → methodproof-0.7.13}/LICENSE +0 -0
  22. {methodproof-0.7.11 → methodproof-0.7.13}/README.md +0 -0
  23. {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/__main__.py +0 -0
  24. {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/_daemon.py +0 -0
  25. {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/agents/__init__.py +0 -0
  26. {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/agents/base.py +0 -0
  27. {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/agents/music.py +0 -0
  28. {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/agents/terminal.py +0 -0
  29. {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/agents/watcher.py +0 -0
  30. {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/analysis.py +0 -0
  31. {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/binding.py +0 -0
  32. {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/bip39.py +0 -0
  33. {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/bridge.py +0 -0
  34. {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/crypto.py +0 -0
  35. {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/e2e.py +0 -0
  36. {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/graph.py +0 -0
  37. {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/hooks/__init__.py +0 -0
  38. {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/hooks/claude_code.py +0 -0
  39. {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/hooks/claude_code.sh +0 -0
  40. {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/hooks/cline_hook.sh +0 -0
  41. {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/hooks/codex_hook.sh +0 -0
  42. {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/hooks/gemini_hook.sh +0 -0
  43. {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/hooks/install.py +0 -0
  44. {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/hooks/kiro_hook.sh +0 -0
  45. {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/hooks/mcp_register.py +0 -0
  46. {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/hooks/openclaw/HOOK.md +0 -0
  47. {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/hooks/openclaw/handler.ts +0 -0
  48. {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/hooks/openclaw_install.py +0 -0
  49. {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/hooks/opencode_plugin.js +0 -0
  50. {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/hooks/wrappers.py +0 -0
  51. {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/integrity.py +0 -0
  52. {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/kdf.py +0 -0
  53. {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/keychain.py +0 -0
  54. {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/live.py +0 -0
  55. {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/lock.py +0 -0
  56. {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/mcp.py +0 -0
  57. {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/migrate_db.py +0 -0
  58. {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/proxy.py +0 -0
  59. {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/proxy_daemon.py +0 -0
  60. {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/repos.py +0 -0
  61. {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/skills/methodproof/SKILL.md +0 -0
  62. {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/store.py +0 -0
  63. {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/viewer.py +0 -0
  64. {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/wordlist.py +0 -0
  65. {methodproof-0.7.11 → methodproof-0.7.13}/test_windows_compat.py +0 -0
  66. {methodproof-0.7.11 → methodproof-0.7.13}/tests/__init__.py +0 -0
  67. {methodproof-0.7.11 → methodproof-0.7.13}/tests/conftest.py +0 -0
  68. {methodproof-0.7.11 → methodproof-0.7.13}/tests/test_analysis.py +0 -0
  69. {methodproof-0.7.11 → methodproof-0.7.13}/tests/test_cli_auth.py +0 -0
  70. {methodproof-0.7.11 → methodproof-0.7.13}/tests/test_cli_config.py +0 -0
  71. {methodproof-0.7.11 → methodproof-0.7.13}/tests/test_cli_helpers.py +0 -0
  72. {methodproof-0.7.11 → methodproof-0.7.13}/tests/test_cli_session.py +0 -0
  73. {methodproof-0.7.11 → methodproof-0.7.13}/tests/test_cli_share.py +0 -0
  74. {methodproof-0.7.11 → methodproof-0.7.13}/tests/test_cli_start.py +0 -0
  75. {methodproof-0.7.11 → methodproof-0.7.13}/tests/test_cli_update.py +0 -0
  76. {methodproof-0.7.11 → methodproof-0.7.13}/tests/test_e2e_integration.py +0 -0
  77. {methodproof-0.7.11 → methodproof-0.7.13}/tests/test_graph.py +0 -0
  78. {methodproof-0.7.11 → methodproof-0.7.13}/tests/test_hooks.py +0 -0
  79. {methodproof-0.7.11 → methodproof-0.7.13}/tests/test_live.py +0 -0
  80. {methodproof-0.7.11 → methodproof-0.7.13}/tests/test_openclaw_hooks.py +0 -0
  81. {methodproof-0.7.11 → methodproof-0.7.13}/tests/test_profiles.py +0 -0
  82. {methodproof-0.7.11 → methodproof-0.7.13}/tests/test_security.py +0 -0
  83. {methodproof-0.7.11 → methodproof-0.7.13}/tests/test_store.py +0 -0
  84. {methodproof-0.7.11 → methodproof-0.7.13}/tests/test_sync.py +0 -0
  85. {methodproof-0.7.11 → methodproof-0.7.13}/tests/test_viewer.py +0 -0
  86. {methodproof-0.7.11 → methodproof-0.7.13}/tests/test_wrappers.py +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.11
3
+ Version: 0.7.13
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.13"
@@ -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()
@@ -448,8 +460,8 @@ def cmd_init(args: argparse.Namespace) -> None:
448
460
  print("Signing key: exists")
449
461
 
450
462
  _print_intro()
451
- print(" To activate now: eval \"$(methodproof shell-hook)\"")
452
- print(" Or restart your shell, then run: mp start\n")
463
+ print(" Restart your shell or run this to activate now:\n")
464
+ print(" eval \"$(methodproof shell-hook)\"\n")
453
465
 
454
466
 
455
467
  def cmd_shell_hook(_args: argparse.Namespace) -> None:
@@ -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
 
@@ -1294,6 +1379,20 @@ def cmd_view(args: argparse.Namespace) -> None:
1294
1379
 
1295
1380
 
1296
1381
  def cmd_log(args: argparse.Namespace) -> None:
1382
+ cfg = config.load()
1383
+ if _resolve_ui(args, cfg):
1384
+ _tui_guard()
1385
+ from methodproof.tui.log import run as tui_log
1386
+ result = tui_log()
1387
+ if result:
1388
+ action, sid = result
1389
+ import argparse as _ap
1390
+ fake = _ap.Namespace(session_id=sid, local=False)
1391
+ if action == "push":
1392
+ cmd_push(fake)
1393
+ elif action == "view":
1394
+ cmd_view(fake)
1395
+ return
1297
1396
  sessions = store.list_sessions()
1298
1397
  if not sessions:
1299
1398
  print("No sessions yet.")
@@ -1318,8 +1417,13 @@ def cmd_log(args: argparse.Namespace) -> None:
1318
1417
 
1319
1418
  def cmd_status(args: argparse.Namespace) -> None:
1320
1419
  """Show auth, session, and config status at a glance."""
1321
- from methodproof import __version__
1322
1420
  cfg = config.load()
1421
+ if _resolve_ui(args, cfg):
1422
+ _tui_guard()
1423
+ from methodproof.tui.status import run as tui_status
1424
+ tui_status(cfg)
1425
+ return
1426
+ from methodproof import __version__
1323
1427
  token = cfg.get("token", "")
1324
1428
  claims = _decode_jwt_claims(token) if token else {}
1325
1429
  sessions = store.list_sessions()
@@ -1663,7 +1767,13 @@ def cmd_delete(args: argparse.Namespace) -> None:
1663
1767
 
1664
1768
  def cmd_review(args: argparse.Namespace) -> None:
1665
1769
  """Show exactly what a session contains before pushing."""
1770
+ cfg = config.load()
1666
1771
  session = _resolve_session(args.session_id)
1772
+ if _resolve_ui(args, cfg):
1773
+ _tui_guard()
1774
+ from methodproof.tui.review import run as tui_review
1775
+ tui_review(session)
1776
+ return
1667
1777
  events = store.get_events(session["id"])
1668
1778
  if not events:
1669
1779
  print("No events in this session.")
@@ -1979,11 +2089,14 @@ def main() -> None:
1979
2089
  s.add_argument("--no-e2e", action="store_true", help="Disable E2E for this session (overrides config)")
1980
2090
  s.add_argument("--verbose", "-v", action="store_true", help="Debug logging at each step (still daemonizes)")
1981
2091
  s.add_argument("--streaming", action="store_true", help="Blocking foreground — stream every captured event to stdout")
2092
+ _add_ui_flags(s)
1982
2093
  sub.add_parser("stop", help="Stop recording")
1983
2094
  v = sub.add_parser("view", help="Inspect captured session data")
1984
2095
  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")
2096
+ l_log = sub.add_parser("log", help="List sessions")
2097
+ _add_ui_flags(l_log)
2098
+ s_status = sub.add_parser("status", help="Auth, session, and config status")
2099
+ _add_ui_flags(s_status)
1987
2100
  l = sub.add_parser("login", help="Connect to platform")
1988
2101
  l.add_argument("--api-url")
1989
2102
  l.add_argument("--force", "-f", action="store_true", help="Skip switch-account prompt")
@@ -2005,7 +2118,14 @@ def main() -> None:
2005
2118
  dl.add_argument("--force", "-f", action="store_true", help="Skip confirmation")
2006
2119
  rv = sub.add_parser("review", help="Review session data before pushing")
2007
2120
  rv.add_argument("session_id", nargs="?")
2008
- sub.add_parser("consent", help="Change capture, research, and redaction settings")
2121
+ _add_ui_flags(rv)
2122
+ c_consent = sub.add_parser("consent", help="Change capture, research, and redaction settings")
2123
+ _add_ui_flags(c_consent)
2124
+ ui_p = sub.add_parser("ui", help="Toggle TUI mode on/off")
2125
+ ui_sub = ui_p.add_subparsers(dest="ui_cmd")
2126
+ ui_sub.add_parser("on", help="Enable TUI mode")
2127
+ ui_sub.add_parser("off", help="Disable TUI mode (classic output)")
2128
+ ui_sub.add_parser("status", help="Show TUI mode and library status")
2009
2129
  up = sub.add_parser("update", help="Update to the latest version from PyPI")
2010
2130
  up_auto = up.add_mutually_exclusive_group()
2011
2131
  up_auto.add_argument("--auto", dest="auto", action="store_true", default=None,
@@ -2062,6 +2182,7 @@ def main() -> None:
2062
2182
  "intro": lambda _: _print_intro(),
2063
2183
  "help": lambda _: _print_commands(),
2064
2184
  "shell-hook": cmd_shell_hook,
2185
+ "ui": cmd_ui,
2065
2186
  "mcp-serve": cmd_mcp_serve,
2066
2187
  "proxy": lambda a: __import__("methodproof.proxy", fromlist=["cmd_proxy"]).cmd_proxy(a),
2067
2188
  }
@@ -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
@@ -78,12 +78,13 @@ def get_shell_rc() -> tuple[Path, str]:
78
78
 
79
79
  def install() -> str:
80
80
  rc, hook_text = get_shell_rc()
81
+ shell = "PowerShell" if sys.platform == "win32" else os.path.basename(os.environ.get("SHELL", "bash"))
81
82
  if rc.exists() and MARKER in rc.read_text():
82
- return f"Already installed in {rc}"
83
+ return f"already installed ({shell}: {rc})"
83
84
  rc.parent.mkdir(parents=True, exist_ok=True)
84
85
  with rc.open("a") as f:
85
86
  f.write(hook_text)
86
- return str(rc)
87
+ return f"{shell}: {rc}"
87
88
 
88
89
 
89
90
  def is_installed() -> bool:
@@ -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