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.
- methodproof-0.7.13/.code-review-graph/.gitignore +3 -0
- methodproof-0.7.13/.code-review-graph/graph.db +0 -0
- {methodproof-0.7.11 → methodproof-0.7.13}/PKG-INFO +4 -1
- {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/__init__.py +1 -1
- {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/cli.py +128 -7
- {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/config.py +1 -0
- {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/hook.py +3 -2
- {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/sync.py +7 -5
- methodproof-0.7.13/methodproof/tui/__init__.py +1 -0
- methodproof-0.7.13/methodproof/tui/consent.py +256 -0
- methodproof-0.7.13/methodproof/tui/log.py +155 -0
- methodproof-0.7.13/methodproof/tui/review.py +137 -0
- methodproof-0.7.13/methodproof/tui/start.py +200 -0
- methodproof-0.7.13/methodproof/tui/status.py +154 -0
- methodproof-0.7.13/methodproof/tui/theme.py +53 -0
- {methodproof-0.7.11 → methodproof-0.7.13}/pyproject.toml +2 -1
- {methodproof-0.7.11 → methodproof-0.7.13}/uv.lock +1 -1
- {methodproof-0.7.11 → methodproof-0.7.13}/.github/workflows/ci.yml +0 -0
- {methodproof-0.7.11 → methodproof-0.7.13}/.gitignore +0 -0
- {methodproof-0.7.11 → methodproof-0.7.13}/CHANGELOG.md +0 -0
- {methodproof-0.7.11 → methodproof-0.7.13}/LICENSE +0 -0
- {methodproof-0.7.11 → methodproof-0.7.13}/README.md +0 -0
- {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/__main__.py +0 -0
- {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/_daemon.py +0 -0
- {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/agents/__init__.py +0 -0
- {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/agents/base.py +0 -0
- {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/agents/music.py +0 -0
- {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/agents/terminal.py +0 -0
- {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/agents/watcher.py +0 -0
- {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/analysis.py +0 -0
- {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/binding.py +0 -0
- {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/bip39.py +0 -0
- {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/bridge.py +0 -0
- {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/crypto.py +0 -0
- {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/e2e.py +0 -0
- {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/graph.py +0 -0
- {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/hooks/__init__.py +0 -0
- {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/hooks/claude_code.py +0 -0
- {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/hooks/claude_code.sh +0 -0
- {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/hooks/cline_hook.sh +0 -0
- {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/hooks/codex_hook.sh +0 -0
- {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/hooks/gemini_hook.sh +0 -0
- {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/hooks/install.py +0 -0
- {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/hooks/kiro_hook.sh +0 -0
- {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/hooks/mcp_register.py +0 -0
- {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/hooks/openclaw/HOOK.md +0 -0
- {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/hooks/openclaw/handler.ts +0 -0
- {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/hooks/openclaw_install.py +0 -0
- {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/hooks/opencode_plugin.js +0 -0
- {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/hooks/wrappers.py +0 -0
- {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/integrity.py +0 -0
- {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/kdf.py +0 -0
- {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/keychain.py +0 -0
- {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/live.py +0 -0
- {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/lock.py +0 -0
- {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/mcp.py +0 -0
- {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/migrate_db.py +0 -0
- {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/proxy.py +0 -0
- {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/proxy_daemon.py +0 -0
- {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/repos.py +0 -0
- {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/skills/methodproof/SKILL.md +0 -0
- {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/store.py +0 -0
- {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/viewer.py +0 -0
- {methodproof-0.7.11 → methodproof-0.7.13}/methodproof/wordlist.py +0 -0
- {methodproof-0.7.11 → methodproof-0.7.13}/test_windows_compat.py +0 -0
- {methodproof-0.7.11 → methodproof-0.7.13}/tests/__init__.py +0 -0
- {methodproof-0.7.11 → methodproof-0.7.13}/tests/conftest.py +0 -0
- {methodproof-0.7.11 → methodproof-0.7.13}/tests/test_analysis.py +0 -0
- {methodproof-0.7.11 → methodproof-0.7.13}/tests/test_cli_auth.py +0 -0
- {methodproof-0.7.11 → methodproof-0.7.13}/tests/test_cli_config.py +0 -0
- {methodproof-0.7.11 → methodproof-0.7.13}/tests/test_cli_helpers.py +0 -0
- {methodproof-0.7.11 → methodproof-0.7.13}/tests/test_cli_session.py +0 -0
- {methodproof-0.7.11 → methodproof-0.7.13}/tests/test_cli_share.py +0 -0
- {methodproof-0.7.11 → methodproof-0.7.13}/tests/test_cli_start.py +0 -0
- {methodproof-0.7.11 → methodproof-0.7.13}/tests/test_cli_update.py +0 -0
- {methodproof-0.7.11 → methodproof-0.7.13}/tests/test_e2e_integration.py +0 -0
- {methodproof-0.7.11 → methodproof-0.7.13}/tests/test_graph.py +0 -0
- {methodproof-0.7.11 → methodproof-0.7.13}/tests/test_hooks.py +0 -0
- {methodproof-0.7.11 → methodproof-0.7.13}/tests/test_live.py +0 -0
- {methodproof-0.7.11 → methodproof-0.7.13}/tests/test_openclaw_hooks.py +0 -0
- {methodproof-0.7.11 → methodproof-0.7.13}/tests/test_profiles.py +0 -0
- {methodproof-0.7.11 → methodproof-0.7.13}/tests/test_security.py +0 -0
- {methodproof-0.7.11 → methodproof-0.7.13}/tests/test_store.py +0 -0
- {methodproof-0.7.11 → methodproof-0.7.13}/tests/test_sync.py +0 -0
- {methodproof-0.7.11 → methodproof-0.7.13}/tests/test_viewer.py +0 -0
- {methodproof-0.7.11 → methodproof-0.7.13}/tests/test_wrappers.py +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.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">
|
|
@@ -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("
|
|
452
|
-
print("
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
|
@@ -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"
|
|
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
|
|
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=
|
|
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
|