methodproof 0.6.0__tar.gz → 0.7.1__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.6.0 → methodproof-0.7.1}/CHANGELOG.md +22 -0
- {methodproof-0.6.0 → methodproof-0.7.1}/PKG-INFO +1 -1
- {methodproof-0.6.0 → methodproof-0.7.1}/methodproof/__init__.py +1 -1
- {methodproof-0.6.0 → methodproof-0.7.1}/methodproof/_daemon.py +16 -2
- {methodproof-0.6.0 → methodproof-0.7.1}/methodproof/agents/base.py +63 -4
- {methodproof-0.6.0 → methodproof-0.7.1}/methodproof/agents/watcher.py +29 -1
- {methodproof-0.6.0 → methodproof-0.7.1}/methodproof/cli.py +158 -25
- {methodproof-0.6.0 → methodproof-0.7.1}/methodproof/config.py +5 -0
- methodproof-0.7.1/methodproof/e2e.py +304 -0
- {methodproof-0.6.0 → methodproof-0.7.1}/methodproof/hooks/claude_code.py +47 -1
- {methodproof-0.6.0 → methodproof-0.7.1}/methodproof/hooks/claude_code.sh +52 -2
- {methodproof-0.6.0 → methodproof-0.7.1}/methodproof/hooks/install.py +14 -0
- {methodproof-0.6.0 → methodproof-0.7.1}/methodproof/proxy_daemon.py +17 -2
- {methodproof-0.6.0 → methodproof-0.7.1}/methodproof/store.py +8 -2
- {methodproof-0.6.0 → methodproof-0.7.1}/methodproof/sync.py +27 -0
- {methodproof-0.6.0 → methodproof-0.7.1}/pyproject.toml +1 -1
- {methodproof-0.6.0 → methodproof-0.7.1}/uv.lock +1 -1
- {methodproof-0.6.0 → methodproof-0.7.1}/.github/workflows/ci.yml +0 -0
- {methodproof-0.6.0 → methodproof-0.7.1}/.gitignore +0 -0
- {methodproof-0.6.0 → methodproof-0.7.1}/LICENSE +0 -0
- {methodproof-0.6.0 → methodproof-0.7.1}/README.md +0 -0
- {methodproof-0.6.0 → methodproof-0.7.1}/methodproof/__main__.py +0 -0
- {methodproof-0.6.0 → methodproof-0.7.1}/methodproof/agents/__init__.py +0 -0
- {methodproof-0.6.0 → methodproof-0.7.1}/methodproof/agents/music.py +0 -0
- {methodproof-0.6.0 → methodproof-0.7.1}/methodproof/agents/terminal.py +0 -0
- {methodproof-0.6.0 → methodproof-0.7.1}/methodproof/analysis.py +0 -0
- {methodproof-0.6.0 → methodproof-0.7.1}/methodproof/binding.py +0 -0
- {methodproof-0.6.0 → methodproof-0.7.1}/methodproof/bip39.py +0 -0
- {methodproof-0.6.0 → methodproof-0.7.1}/methodproof/bridge.py +0 -0
- {methodproof-0.6.0 → methodproof-0.7.1}/methodproof/crypto.py +0 -0
- {methodproof-0.6.0 → methodproof-0.7.1}/methodproof/graph.py +0 -0
- {methodproof-0.6.0 → methodproof-0.7.1}/methodproof/hook.py +0 -0
- {methodproof-0.6.0 → methodproof-0.7.1}/methodproof/hooks/__init__.py +0 -0
- {methodproof-0.6.0 → methodproof-0.7.1}/methodproof/hooks/cline_hook.sh +0 -0
- {methodproof-0.6.0 → methodproof-0.7.1}/methodproof/hooks/codex_hook.sh +0 -0
- {methodproof-0.6.0 → methodproof-0.7.1}/methodproof/hooks/gemini_hook.sh +0 -0
- {methodproof-0.6.0 → methodproof-0.7.1}/methodproof/hooks/kiro_hook.sh +0 -0
- {methodproof-0.6.0 → methodproof-0.7.1}/methodproof/hooks/mcp_register.py +0 -0
- {methodproof-0.6.0 → methodproof-0.7.1}/methodproof/hooks/openclaw/HOOK.md +0 -0
- {methodproof-0.6.0 → methodproof-0.7.1}/methodproof/hooks/openclaw/handler.ts +0 -0
- {methodproof-0.6.0 → methodproof-0.7.1}/methodproof/hooks/openclaw_install.py +0 -0
- {methodproof-0.6.0 → methodproof-0.7.1}/methodproof/hooks/opencode_plugin.js +0 -0
- {methodproof-0.6.0 → methodproof-0.7.1}/methodproof/hooks/wrappers.py +0 -0
- {methodproof-0.6.0 → methodproof-0.7.1}/methodproof/integrity.py +0 -0
- {methodproof-0.6.0 → methodproof-0.7.1}/methodproof/kdf.py +0 -0
- {methodproof-0.6.0 → methodproof-0.7.1}/methodproof/keychain.py +0 -0
- {methodproof-0.6.0 → methodproof-0.7.1}/methodproof/live.py +0 -0
- {methodproof-0.6.0 → methodproof-0.7.1}/methodproof/lock.py +0 -0
- {methodproof-0.6.0 → methodproof-0.7.1}/methodproof/mcp.py +0 -0
- {methodproof-0.6.0 → methodproof-0.7.1}/methodproof/migrate_db.py +0 -0
- {methodproof-0.6.0 → methodproof-0.7.1}/methodproof/proxy.py +0 -0
- {methodproof-0.6.0 → methodproof-0.7.1}/methodproof/repos.py +0 -0
- {methodproof-0.6.0 → methodproof-0.7.1}/methodproof/skills/methodproof/SKILL.md +0 -0
- {methodproof-0.6.0 → methodproof-0.7.1}/methodproof/viewer.py +0 -0
- {methodproof-0.6.0 → methodproof-0.7.1}/methodproof/wordlist.py +0 -0
- {methodproof-0.6.0 → methodproof-0.7.1}/test_windows_compat.py +0 -0
- {methodproof-0.6.0 → methodproof-0.7.1}/tests/__init__.py +0 -0
- {methodproof-0.6.0 → methodproof-0.7.1}/tests/test_analysis.py +0 -0
- {methodproof-0.6.0 → methodproof-0.7.1}/tests/test_graph.py +0 -0
- {methodproof-0.6.0 → methodproof-0.7.1}/tests/test_hooks.py +0 -0
- {methodproof-0.6.0 → methodproof-0.7.1}/tests/test_live.py +0 -0
- {methodproof-0.6.0 → methodproof-0.7.1}/tests/test_openclaw_hooks.py +0 -0
- {methodproof-0.6.0 → methodproof-0.7.1}/tests/test_security.py +0 -0
- {methodproof-0.6.0 → methodproof-0.7.1}/tests/test_store.py +0 -0
- {methodproof-0.6.0 → methodproof-0.7.1}/tests/test_wrappers.py +0 -0
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.7.1] — 2026-04-07
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- **7 GB database bloat** — `artifacts` table lacked a UNIQUE constraint on `path`, causing `_ensure_artifact` to insert a duplicate row per `file_edit`. The `_link_action_artifacts` JOIN then produced a cartesian explosion (39.9M rows from 16K events). Added UNIQUE constraint + migration that deduplicates existing data on upgrade.
|
|
7
|
+
- **Stale PID after reboot** — `_is_daemon_alive()` only checked `os.kill(pid, 0)`, which succeeds if the OS reused the PID for an unrelated process. Now verifies the process name contains "methodproof" via `ps`. Prevents killing random processes and unblocks `mp stop`/`mp start` after a system restart.
|
|
8
|
+
- **SQLite hang on locked database** — `sqlite3.connect()` had no timeout, so a crashed daemon holding a WAL lock caused `mp stop` to hang forever. Added `timeout=10`.
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **Research consent sync** — `mp init`, `mp consent`, `mp login`, and `mp push` now sync research opt-in state between CLI and platform. Local changes are flagged with `_pending_research_sync` and pushed on next authenticated call; canonical state is always pulled from the platform.
|
|
12
|
+
|
|
13
|
+
## [0.7.0] — 2026-04-07
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- **Runaway event logging** — watcher ignored only 7 patterns, tracking 35K+ files including `.venv/` (20K), `dist/build/` (23K). Expanded to 30+ ignore patterns covering Python, JS/TS, Rust, Go, Java, Ruby, PHP, .NET, Swift, and build artifacts. **138K → 1,442 files (99% reduction)**
|
|
17
|
+
- **Claude Code hook errors** — hook script pointed to deleted pyenv site-packages path; added pidfile guard (`exit 0` when no session active) to prevent bridge connection failures
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
- **Configurable local AI ports** — `mp init` now asks about local LLM servers (Ollama, LM Studio, vLLM, etc.) and adds user-specified ports to the proxy allowlist. Previously only 3 hardcoded ports (11434, 18789, 1234) were captured; custom ports were invisible to the proxy decoder.
|
|
21
|
+
- `mp start --streaming` — blocking foreground mode, streams every captured event to stdout in real-time with human-readable formatting
|
|
22
|
+
- `mp start --verbose` / `-v` — structured debug logging at each step (config, auth, session, anchor, daemon spawn); daemon log includes agent init and buffer/flush stats
|
|
23
|
+
- Step-by-step progress output on default `mp start` (`→ Loading config`, `→ Checking hooks`, etc.) so failures are immediately locatable
|
|
24
|
+
|
|
3
25
|
## [0.5.1] — 2026-04-06
|
|
4
26
|
|
|
5
27
|
### Fixed
|
|
@@ -18,6 +18,7 @@ PIDFILE = config.DIR / "methodproof.pid"
|
|
|
18
18
|
def main() -> None:
|
|
19
19
|
sid = sys.argv[1]
|
|
20
20
|
watch_dir = sys.argv[2]
|
|
21
|
+
verbose = "--verbose" in sys.argv
|
|
21
22
|
|
|
22
23
|
cfg = config.load()
|
|
23
24
|
capture = cfg.get("capture", {})
|
|
@@ -28,10 +29,15 @@ def main() -> None:
|
|
|
28
29
|
del cfg["_live_url"]
|
|
29
30
|
config.save(cfg)
|
|
30
31
|
|
|
31
|
-
base.init(sid, live=bool(live_url))
|
|
32
|
+
base.init(sid, live=bool(live_url), verbose=verbose)
|
|
33
|
+
|
|
34
|
+
if verbose:
|
|
35
|
+
active = [k for k, v in capture.items() if v]
|
|
36
|
+
base.log("info", "daemon.config", capture=active, live=bool(live_url),
|
|
37
|
+
journal=cfg.get("journal_mode", False))
|
|
32
38
|
|
|
33
|
-
# Environment profile (was done pre-fork in old code, now done in daemon)
|
|
34
39
|
if capture.get("environment_analysis", True):
|
|
40
|
+
base.log("info", "daemon.agent.environment_scan") if verbose else None
|
|
35
41
|
try:
|
|
36
42
|
from methodproof.analysis import scan_environment
|
|
37
43
|
env_profile = scan_environment(watch_dir)
|
|
@@ -52,12 +58,16 @@ def main() -> None:
|
|
|
52
58
|
threads.append(threading.Thread(
|
|
53
59
|
target=watcher.start, args=(watch_dir, stop_event), daemon=True,
|
|
54
60
|
))
|
|
61
|
+
if verbose:
|
|
62
|
+
base.log("info", "daemon.agent.watcher", dir=watch_dir)
|
|
55
63
|
|
|
56
64
|
if capture.get("terminal_commands", True) or capture.get("test_results", True):
|
|
57
65
|
from methodproof.agents import terminal
|
|
58
66
|
threads.append(threading.Thread(
|
|
59
67
|
target=terminal.start, args=(stop_event,), daemon=True,
|
|
60
68
|
))
|
|
69
|
+
if verbose:
|
|
70
|
+
base.log("info", "daemon.agent.terminal", log=str(config.CMD_LOG))
|
|
61
71
|
|
|
62
72
|
if capture.get("browser", True):
|
|
63
73
|
from methodproof import bridge
|
|
@@ -68,12 +78,16 @@ def main() -> None:
|
|
|
68
78
|
cfg.get("journal_mode", False)),
|
|
69
79
|
daemon=True,
|
|
70
80
|
))
|
|
81
|
+
if verbose:
|
|
82
|
+
base.log("info", "daemon.agent.bridge", port=9877)
|
|
71
83
|
|
|
72
84
|
if capture.get("music", True):
|
|
73
85
|
from methodproof.agents import music
|
|
74
86
|
threads.append(threading.Thread(
|
|
75
87
|
target=music.start, args=(stop_event,), daemon=True,
|
|
76
88
|
))
|
|
89
|
+
if verbose:
|
|
90
|
+
base.log("info", "daemon.agent.music")
|
|
77
91
|
|
|
78
92
|
def _shutdown(sig_num: int, frame: object) -> None:
|
|
79
93
|
stop_event.set()
|
|
@@ -21,6 +21,8 @@ _MAX_RETRIES = 3
|
|
|
21
21
|
_prev_hash = "genesis"
|
|
22
22
|
_account_id = ""
|
|
23
23
|
_journal_mode = False
|
|
24
|
+
_verbose = False
|
|
25
|
+
_streaming = False
|
|
24
26
|
|
|
25
27
|
# Maps event types to the capture category that gates them
|
|
26
28
|
_EVENT_GATES: dict[str, str] = {
|
|
@@ -78,8 +80,15 @@ _FIELD_GATES: dict[str, list[tuple[str, str]]] = {
|
|
|
78
80
|
|
|
79
81
|
|
|
80
82
|
def _load_encryption_key(cfg: dict) -> bytes | None:
|
|
81
|
-
"""Load db_key from keychain
|
|
83
|
+
"""Load encryption key: individual E2E > db_key from keychain > legacy e2e_key."""
|
|
82
84
|
account_id = cfg.get("account_id", "")
|
|
85
|
+
# Individual E2E key — highest priority when session is E2E
|
|
86
|
+
if account_id and cfg.get("e2e_fingerprint") and cfg.get("_session_e2e"):
|
|
87
|
+
from methodproof.keychain import load_secret
|
|
88
|
+
e2e_key = load_secret(f"e2e:{account_id}")
|
|
89
|
+
if e2e_key:
|
|
90
|
+
return e2e_key
|
|
91
|
+
# Local db_key (for SQLite encryption)
|
|
83
92
|
if account_id and cfg.get("master_key_fingerprint"):
|
|
84
93
|
from methodproof.keychain import load_secret
|
|
85
94
|
from methodproof.kdf import derive_master, derive_db_key
|
|
@@ -91,11 +100,13 @@ def _load_encryption_key(cfg: dict) -> bytes | None:
|
|
|
91
100
|
return bytes.fromhex(raw) if raw else None
|
|
92
101
|
|
|
93
102
|
|
|
94
|
-
def init(session_id: str, live: bool = False) -> None:
|
|
95
|
-
global _session_id, _initialized, _e2e_key, _capture, _live_mode, _prev_hash, _journal_mode, _account_id
|
|
103
|
+
def init(session_id: str, live: bool = False, verbose: bool = False, streaming: bool = False) -> None:
|
|
104
|
+
global _session_id, _initialized, _e2e_key, _capture, _live_mode, _prev_hash, _journal_mode, _account_id, _verbose, _streaming
|
|
96
105
|
_session_id = session_id
|
|
97
106
|
_initialized = True
|
|
98
107
|
_live_mode = live
|
|
108
|
+
_verbose = verbose
|
|
109
|
+
_streaming = streaming
|
|
99
110
|
_prev_hash = "genesis"
|
|
100
111
|
from methodproof import config
|
|
101
112
|
cfg = config.load()
|
|
@@ -103,6 +114,10 @@ def init(session_id: str, live: bool = False) -> None:
|
|
|
103
114
|
_capture = cfg.get("capture", {})
|
|
104
115
|
_journal_mode = cfg.get("journal_mode", False)
|
|
105
116
|
_account_id = cfg.get("account_id", "")
|
|
117
|
+
if _verbose or _streaming:
|
|
118
|
+
active = [k for k, v in _capture.items() if v]
|
|
119
|
+
log("info", "base.init", encryption=bool(_e2e_key), journal=_journal_mode,
|
|
120
|
+
live=_live_mode, capture=active)
|
|
106
121
|
|
|
107
122
|
|
|
108
123
|
def log(level: str, event: str, **kw: object) -> None:
|
|
@@ -153,6 +168,10 @@ def emit(event_type: str, metadata: dict[str, Any]) -> None:
|
|
|
153
168
|
_buffer.append(entry)
|
|
154
169
|
if len(_buffer) >= _FLUSH_SIZE:
|
|
155
170
|
_flush_locked()
|
|
171
|
+
if _verbose:
|
|
172
|
+
log("debug", "emit.buffered", type=event_type, buffer=len(_buffer))
|
|
173
|
+
if _streaming:
|
|
174
|
+
_stream_event(entry)
|
|
156
175
|
if _live_mode:
|
|
157
176
|
from methodproof import live as live_mod
|
|
158
177
|
live_mod.send(entry)
|
|
@@ -168,15 +187,55 @@ def _flush_locked() -> None:
|
|
|
168
187
|
return
|
|
169
188
|
batch = list(_buffer)
|
|
170
189
|
hashes = [(e["id"], e.pop("_chain_hash")) for e in batch if "_chain_hash" in e]
|
|
190
|
+
if _verbose or _streaming:
|
|
191
|
+
types = [e["type"] for e in batch]
|
|
192
|
+
log("info", "flush.start", count=len(batch), types=types)
|
|
171
193
|
for attempt in range(_MAX_RETRIES):
|
|
172
194
|
try:
|
|
173
195
|
store.insert_events(_session_id, batch)
|
|
174
196
|
if hashes:
|
|
175
197
|
store.insert_event_hashes(hashes)
|
|
198
|
+
if _verbose or _streaming:
|
|
199
|
+
log("info", "flush.ok", count=len(batch))
|
|
176
200
|
_buffer.clear()
|
|
177
201
|
return
|
|
178
202
|
except Exception as exc:
|
|
179
203
|
log("warning", "flush.retry", attempt=attempt + 1, error=str(exc))
|
|
180
204
|
time.sleep(0.1 * (attempt + 1))
|
|
181
|
-
# Final attempt failed — keep events in buffer for next flush cycle
|
|
182
205
|
log("error", "flush.failed", count=len(batch), retries=_MAX_RETRIES)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _stream_event(entry: dict[str, Any]) -> None:
|
|
209
|
+
"""Print a human-readable event line to stdout for --streaming mode."""
|
|
210
|
+
ts = time.strftime("%H:%M:%S", time.localtime(entry["timestamp"]))
|
|
211
|
+
etype = entry["type"]
|
|
212
|
+
meta = entry.get("metadata", {})
|
|
213
|
+
# Build a compact summary per event type
|
|
214
|
+
detail = ""
|
|
215
|
+
if etype == "file_edit":
|
|
216
|
+
detail = f'{meta.get("path", "?")} +{meta.get("lines_added", 0)}-{meta.get("lines_removed", 0)}'
|
|
217
|
+
elif etype == "file_create":
|
|
218
|
+
detail = f'{meta.get("path", "?")} ({meta.get("size", 0)}B)'
|
|
219
|
+
elif etype == "file_delete":
|
|
220
|
+
detail = meta.get("path", "?")
|
|
221
|
+
elif etype == "terminal_cmd":
|
|
222
|
+
detail = f'{meta.get("command", "?")[:60]} → exit {meta.get("exit_code", "?")}'
|
|
223
|
+
elif etype == "test_run":
|
|
224
|
+
detail = f'{meta.get("framework", "?")} {meta.get("passed", 0)}✓ {meta.get("failed", 0)}✗'
|
|
225
|
+
elif etype == "git_commit":
|
|
226
|
+
detail = f'{meta.get("hash", "?")} {meta.get("message", "")[:50]}'
|
|
227
|
+
elif etype in ("llm_prompt", "agent_prompt", "user_prompt"):
|
|
228
|
+
detail = f'len={meta.get("prompt_length", meta.get("message_length", "?"))}'
|
|
229
|
+
elif etype in ("llm_completion", "agent_completion"):
|
|
230
|
+
detail = f'len={meta.get("response_length", "?")}'
|
|
231
|
+
elif etype == "music_playing":
|
|
232
|
+
detail = f'{meta.get("artist", "?")} — {meta.get("track", "?")}'
|
|
233
|
+
elif etype.startswith("browser_"):
|
|
234
|
+
detail = meta.get("url", meta.get("query", ""))[:60]
|
|
235
|
+
elif etype == "environment_profile":
|
|
236
|
+
detail = f'{meta.get("tool_count", "?")} tools'
|
|
237
|
+
else:
|
|
238
|
+
keys = list(meta.keys())[:4]
|
|
239
|
+
detail = ", ".join(f"{k}={meta[k]}" for k in keys) if keys else ""
|
|
240
|
+
sys.stdout.write(f" [{ts}] {etype:30s} {detail}\n")
|
|
241
|
+
sys.stdout.flush()
|
|
@@ -13,7 +13,35 @@ from watchdog.observers import Observer
|
|
|
13
13
|
from methodproof.agents import base
|
|
14
14
|
|
|
15
15
|
IGNORE_PATTERNS = re.compile(
|
|
16
|
-
|
|
16
|
+
# Version control
|
|
17
|
+
r"(\.git/|\.hg/|\.svn/"
|
|
18
|
+
# OS / editor artifacts
|
|
19
|
+
r"|\.DS_Store|Thumbs\.db|\.swp|\.swo|~$|\.idea/|\.vscode/"
|
|
20
|
+
# Python
|
|
21
|
+
r"|__pycache__|\.pyc|\.pyo|\.egg-info/|\.eggs/|\.tox/"
|
|
22
|
+
r"|\.venv/|venv/|\.env/|env/|\.mypy_cache/|\.pytest_cache/|\.ruff_cache/"
|
|
23
|
+
r"|\.coverage|htmlcov/|\.nox/"
|
|
24
|
+
# JavaScript / TypeScript
|
|
25
|
+
r"|node_modules/|\.next/|\.nuxt/|\.expo/|\.turbo/|\.parcel-cache/"
|
|
26
|
+
r"|\.svelte-kit/|\.angular/|\.cache/"
|
|
27
|
+
# Rust
|
|
28
|
+
r"|/target/|\.cargo/"
|
|
29
|
+
# Go
|
|
30
|
+
r"|vendor/"
|
|
31
|
+
# Java / Kotlin / JVM
|
|
32
|
+
r"|\.gradle/|\.m2/|/out/"
|
|
33
|
+
# Ruby
|
|
34
|
+
r"|\.bundle/|\.gem/"
|
|
35
|
+
# PHP
|
|
36
|
+
r"|/vendor/|\.phpunit/"
|
|
37
|
+
# .NET / C#
|
|
38
|
+
r"|/bin/|/obj/|\.nuget/"
|
|
39
|
+
# Swift / Xcode
|
|
40
|
+
r"|\.build/|DerivedData/|Pods/"
|
|
41
|
+
# Build output / artifacts
|
|
42
|
+
r"|dist/|build/|\.output/"
|
|
43
|
+
# Logs and locks
|
|
44
|
+
r"|\.lock$|\.log$)"
|
|
17
45
|
)
|
|
18
46
|
|
|
19
47
|
|
|
@@ -337,6 +337,11 @@ def cmd_init(args: argparse.Namespace) -> None:
|
|
|
337
337
|
if not cfg.get("consent_acknowledged"):
|
|
338
338
|
cfg = _run_consent(cfg)
|
|
339
339
|
config.save(cfg)
|
|
340
|
+
if cfg.get("token"):
|
|
341
|
+
cfg["_pending_research_sync"] = True
|
|
342
|
+
config.save(cfg)
|
|
343
|
+
from methodproof.sync import sync_research_consent
|
|
344
|
+
sync_research_consent(cfg["token"], cfg["api_url"])
|
|
340
345
|
print()
|
|
341
346
|
|
|
342
347
|
capture = cfg.get("capture", {})
|
|
@@ -408,6 +413,23 @@ def cmd_init(args: argparse.Namespace) -> None:
|
|
|
408
413
|
else:
|
|
409
414
|
print("AI hooks: skipped (ai_prompts and ai_responses disabled)")
|
|
410
415
|
|
|
416
|
+
# Local AI ports — capture traffic from local LLM servers
|
|
417
|
+
if ai_enabled and not cfg.get("local_ai_ports_offered"):
|
|
418
|
+
answer = input("Run any local AI models (Ollama, LM Studio, vLLM, etc.)? [y/N]: ").strip().lower()
|
|
419
|
+
cfg["local_ai_ports_offered"] = True
|
|
420
|
+
if answer == "y":
|
|
421
|
+
raw = input("Enter ports (comma-separated, e.g. 8080,5000,7860): ").strip()
|
|
422
|
+
ports = [int(p.strip()) for p in raw.split(",") if p.strip().isdigit()]
|
|
423
|
+
cfg["local_ai_ports"] = ports
|
|
424
|
+
if ports:
|
|
425
|
+
print(f"Local AI ports: {', '.join(str(p) for p in ports)} (proxy will decode these)")
|
|
426
|
+
else:
|
|
427
|
+
print("Local AI ports: none added")
|
|
428
|
+
else:
|
|
429
|
+
cfg["local_ai_ports"] = []
|
|
430
|
+
print("Local AI ports: skipped (built-in: Ollama 11434, Jan 1234)")
|
|
431
|
+
config.save(cfg)
|
|
432
|
+
|
|
411
433
|
# Signing keypair for attestation
|
|
412
434
|
from methodproof.integrity import has_keypair
|
|
413
435
|
if not has_keypair():
|
|
@@ -661,6 +683,11 @@ def cmd_consent(args: argparse.Namespace) -> None:
|
|
|
661
683
|
print(f"\n{_banner()}\n")
|
|
662
684
|
cfg = _run_consent_detailed(cfg)
|
|
663
685
|
config.save(cfg)
|
|
686
|
+
if cfg.get("token"):
|
|
687
|
+
cfg["_pending_research_sync"] = True
|
|
688
|
+
config.save(cfg)
|
|
689
|
+
from methodproof.sync import sync_research_consent
|
|
690
|
+
sync_research_consent(cfg["token"], cfg["api_url"])
|
|
664
691
|
print(f"\n{_banner()} settings saved.\n")
|
|
665
692
|
_print_commands()
|
|
666
693
|
|
|
@@ -834,39 +861,71 @@ def _recover_master_key(cfg: dict, account_id: str) -> None:
|
|
|
834
861
|
|
|
835
862
|
|
|
836
863
|
def _is_daemon_alive() -> bool:
|
|
837
|
-
"""Check if the recording daemon is still running."""
|
|
864
|
+
"""Check if the recording daemon is still running (not a reused PID)."""
|
|
838
865
|
if not PIDFILE.exists():
|
|
839
866
|
return False
|
|
840
867
|
try:
|
|
841
868
|
pid = int(PIDFILE.read_text().strip())
|
|
842
|
-
os.kill(pid, 0)
|
|
843
|
-
return True
|
|
869
|
+
os.kill(pid, 0)
|
|
844
870
|
except (ProcessLookupError, ValueError, OSError):
|
|
845
871
|
return False
|
|
872
|
+
# Verify the PID is actually a methodproof process (PIDs get reused after reboot)
|
|
873
|
+
try:
|
|
874
|
+
import subprocess
|
|
875
|
+
out = subprocess.check_output(["ps", "-p", str(pid), "-o", "args="], text=True).strip()
|
|
876
|
+
return "methodproof" in out
|
|
877
|
+
except Exception:
|
|
878
|
+
return False
|
|
879
|
+
|
|
880
|
+
|
|
881
|
+
def _log_step(msg: str, verbose: bool = False) -> None:
|
|
882
|
+
"""Print a step-by-step progress line. Always shown."""
|
|
883
|
+
print(f" → {msg}")
|
|
884
|
+
|
|
885
|
+
|
|
886
|
+
def _log_debug(msg: str, **kw: object) -> None:
|
|
887
|
+
"""Print structured debug line (--verbose only). Writes to stderr."""
|
|
888
|
+
import json as _json
|
|
889
|
+
entry = {"ts": time.time(), "level": "debug", "event": msg, **kw}
|
|
890
|
+
sys.stderr.write(_json.dumps(entry, default=str) + "\n")
|
|
846
891
|
|
|
847
892
|
|
|
848
893
|
def cmd_start(args: argparse.Namespace) -> None:
|
|
894
|
+
verbose = getattr(args, "verbose", False)
|
|
895
|
+
streaming = getattr(args, "streaming", False)
|
|
896
|
+
|
|
897
|
+
_log_step("Loading config")
|
|
849
898
|
cfg = config.load()
|
|
899
|
+
if verbose:
|
|
900
|
+
_log_debug("config.loaded", api_url=cfg.get("api_url"), account_id=cfg.get("account_id", "")[:8],
|
|
901
|
+
active_session=cfg.get("active_session"), journal=cfg.get("journal_mode"))
|
|
902
|
+
|
|
850
903
|
if cfg.get("auto_update"):
|
|
904
|
+
_log_step("Checking for updates")
|
|
851
905
|
_auto_update()
|
|
906
|
+
|
|
852
907
|
if cfg.get("active_session"):
|
|
853
908
|
if _is_daemon_alive():
|
|
854
909
|
print(f"Session active: {cfg['active_session'][:8]}")
|
|
855
910
|
print("Run `methodproof stop` first.")
|
|
856
911
|
sys.exit(1)
|
|
857
|
-
# Daemon is dead — clean up the stale session
|
|
858
912
|
stale_sid = cfg["active_session"]
|
|
913
|
+
_log_step(f"Cleaning stale session {stale_sid[:8]}")
|
|
859
914
|
store.complete_session(stale_sid)
|
|
860
915
|
graph.build(stale_sid)
|
|
861
916
|
PIDFILE.unlink(missing_ok=True)
|
|
862
917
|
cfg["active_session"] = None
|
|
863
918
|
config.save(cfg)
|
|
864
|
-
|
|
919
|
+
|
|
920
|
+
_log_step("Checking hooks")
|
|
865
921
|
if not hook.is_installed():
|
|
866
|
-
print("Run `methodproof init` first.")
|
|
922
|
+
print("ERROR: Run `methodproof init` first.")
|
|
867
923
|
sys.exit(1)
|
|
868
924
|
|
|
925
|
+
_log_step("Authenticating")
|
|
869
926
|
account_id = _require_auth(cfg)
|
|
927
|
+
if verbose:
|
|
928
|
+
_log_debug("auth.ok", account_id=account_id[:8] if account_id else "none")
|
|
870
929
|
|
|
871
930
|
# Check for new consent categories before recording
|
|
872
931
|
capture = cfg.get("capture", {})
|
|
@@ -878,6 +937,7 @@ def cmd_start(args: argparse.Namespace) -> None:
|
|
|
878
937
|
config.save(cfg)
|
|
879
938
|
print()
|
|
880
939
|
|
|
940
|
+
_log_step("Creating session")
|
|
881
941
|
sid = uuid.uuid4().hex
|
|
882
942
|
watch_dir = os.path.abspath(args.dir or ".")
|
|
883
943
|
repo_url = args.repo or repos.detect_repo(watch_dir)
|
|
@@ -885,7 +945,6 @@ def cmd_start(args: argparse.Namespace) -> None:
|
|
|
885
945
|
visibility = "public" if args.public else "private"
|
|
886
946
|
from methodproof.binding import compute_binding, compute_device_id
|
|
887
947
|
device_id = compute_device_id()
|
|
888
|
-
# Compute session binding if master key is available
|
|
889
948
|
binding = ""
|
|
890
949
|
if cfg.get("master_key_fingerprint") and account_id:
|
|
891
950
|
from methodproof.keychain import load_secret
|
|
@@ -895,38 +954,49 @@ def cmd_start(args: argparse.Namespace) -> None:
|
|
|
895
954
|
master = derive_master(entropy)
|
|
896
955
|
bind_key = derive_bind_key(master, account_id)
|
|
897
956
|
binding = compute_binding(bind_key, sid, account_id, device_id, time.time())
|
|
957
|
+
if verbose:
|
|
958
|
+
_log_debug("binding.computed", device_id=device_id[:8])
|
|
898
959
|
store.create_session(sid, watch_dir, repo_url, json.dumps(tags), visibility,
|
|
899
960
|
account_id, binding, device_id)
|
|
900
961
|
cfg["active_session"] = sid
|
|
901
962
|
config.save(cfg)
|
|
902
963
|
PIDFILE.write_text(str(os.getpid()))
|
|
964
|
+
if verbose:
|
|
965
|
+
_log_debug("session.created", sid=sid[:8], watch_dir=watch_dir, visibility=visibility,
|
|
966
|
+
device_id=device_id[:8], bound=bool(binding))
|
|
903
967
|
|
|
904
|
-
# Temporal anchor
|
|
968
|
+
# Temporal anchor
|
|
905
969
|
if cfg.get("token"):
|
|
970
|
+
_log_step("Requesting temporal anchor")
|
|
906
971
|
try:
|
|
907
972
|
from methodproof.sync import _request
|
|
908
973
|
anchor = _request("POST", f"/sessions/{sid}/anchor", cfg["api_url"], cfg["token"])
|
|
909
974
|
store.update_anchor(sid, anchor["anchor_ts"], anchor["signature"])
|
|
975
|
+
if verbose:
|
|
976
|
+
_log_debug("anchor.ok", anchor_ts=anchor["anchor_ts"])
|
|
910
977
|
except Exception as exc:
|
|
911
|
-
|
|
978
|
+
_log_step(f"Anchor: skipped ({exc})")
|
|
912
979
|
|
|
913
980
|
from methodproof.agents import base
|
|
914
|
-
live_ok = False
|
|
915
981
|
capture = cfg.get("capture", {})
|
|
916
982
|
|
|
983
|
+
# Live streaming
|
|
917
984
|
live_url = ""
|
|
918
985
|
want_live = args.live or getattr(args, "live_public", False)
|
|
919
986
|
live_visibility = "public" if getattr(args, "live_public", False) else "private"
|
|
920
987
|
if want_live:
|
|
988
|
+
_log_step("Connecting live stream")
|
|
921
989
|
if not cfg.get("token"):
|
|
922
990
|
print("Live mode requires login. Run `methodproof login` first.")
|
|
923
991
|
sys.exit(1)
|
|
924
|
-
# Create remote session first
|
|
925
992
|
from methodproof.sync import _request
|
|
926
|
-
|
|
993
|
+
session_body: dict = {}
|
|
994
|
+
if cfg.get("e2e_fingerprint") and (getattr(args, "e2e", False) or cfg.get("e2e_mode")):
|
|
995
|
+
session_body["e2e_key_fingerprint"] = cfg["e2e_fingerprint"]
|
|
996
|
+
result = _request("POST", "/personal/sessions", cfg["api_url"], cfg["token"],
|
|
997
|
+
session_body or None)
|
|
927
998
|
remote_id = result["session_id"]
|
|
928
999
|
store.mark_synced(sid, remote_id)
|
|
929
|
-
# Connect live WebSocket
|
|
930
1000
|
from methodproof import live as live_mod
|
|
931
1001
|
live_url = live_mod.start(cfg["api_url"], cfg["token"], remote_id, capture, live_visibility) or ""
|
|
932
1002
|
if not live_url:
|
|
@@ -939,7 +1009,7 @@ def cmd_start(args: argparse.Namespace) -> None:
|
|
|
939
1009
|
print(f"Live (public): {live_url}")
|
|
940
1010
|
print(" Anyone with this link can watch your session build in real time.")
|
|
941
1011
|
|
|
942
|
-
# Journal mode
|
|
1012
|
+
# Journal mode
|
|
943
1013
|
if getattr(args, "journal", False):
|
|
944
1014
|
cfg["journal_mode"] = True
|
|
945
1015
|
credits = cfg.get("journal_credits", 0)
|
|
@@ -950,6 +1020,18 @@ def cmd_start(args: argparse.Namespace) -> None:
|
|
|
950
1020
|
print("Journal mode ON for this session (full content capture).")
|
|
951
1021
|
config.save(cfg)
|
|
952
1022
|
|
|
1023
|
+
# E2E mode
|
|
1024
|
+
want_e2e = getattr(args, "e2e", False) or (cfg.get("e2e_mode") and not getattr(args, "no_e2e", False))
|
|
1025
|
+
if want_e2e:
|
|
1026
|
+
fp = cfg.get("e2e_fingerprint")
|
|
1027
|
+
if not fp:
|
|
1028
|
+
print("E2E requires key setup. Run `methodproof e2e on` first.")
|
|
1029
|
+
sys.exit(1)
|
|
1030
|
+
cfg["_session_e2e"] = True
|
|
1031
|
+
config.save(cfg)
|
|
1032
|
+
print("E2E mode ON for this session (content encrypted with your key).")
|
|
1033
|
+
print(" Narration unavailable. Release later with: mp e2e release <session-id>")
|
|
1034
|
+
|
|
953
1035
|
# Save live_url for daemon subprocess to pick up
|
|
954
1036
|
if live_url:
|
|
955
1037
|
cfg["_live_url"] = live_url
|
|
@@ -966,25 +1048,35 @@ def cmd_start(args: argparse.Namespace) -> None:
|
|
|
966
1048
|
print(f"Bridge: http://localhost:9877")
|
|
967
1049
|
if live_url:
|
|
968
1050
|
print(f"Live: {live_url}")
|
|
1051
|
+
if verbose:
|
|
1052
|
+
print(f"Mode: verbose (debug → daemon.log)")
|
|
1053
|
+
if streaming:
|
|
1054
|
+
print(f"Mode: streaming (blocking, events → stdout)")
|
|
1055
|
+
|
|
1056
|
+
# --streaming: blocking foreground mode with real-time event output
|
|
1057
|
+
if streaming:
|
|
1058
|
+
_run_foreground(sid, watch_dir, cfg, capture, live_url, verbose=True, streaming=True)
|
|
1059
|
+
return
|
|
969
1060
|
|
|
970
|
-
# Daemonize
|
|
1061
|
+
# Daemonize (macOS/Linux) — pass --verbose to daemon if set
|
|
971
1062
|
if sys.platform != "win32":
|
|
972
1063
|
import subprocess as _sp
|
|
973
1064
|
daemon_log = config.DIR / "daemon.log"
|
|
974
1065
|
log_fh = open(daemon_log, "a")
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
)
|
|
1066
|
+
cmd = [sys.executable, "-m", "methodproof._daemon", sid, watch_dir]
|
|
1067
|
+
if verbose:
|
|
1068
|
+
cmd.append("--verbose")
|
|
1069
|
+
_log_step(f"Spawning daemon (log: {daemon_log})")
|
|
1070
|
+
proc = _sp.Popen(cmd, start_new_session=True, stdout=log_fh, stderr=log_fh, stdin=_sp.DEVNULL)
|
|
980
1071
|
PIDFILE.write_text(str(proc.pid))
|
|
981
|
-
|
|
1072
|
+
if verbose:
|
|
1073
|
+
_log_debug("daemon.spawned", pid=proc.pid, cmd=cmd)
|
|
982
1074
|
time.sleep(1)
|
|
983
1075
|
if proc.poll() is not None:
|
|
984
1076
|
print(f"ERROR: Daemon exited immediately (code {proc.returncode}). Check {daemon_log}")
|
|
985
1077
|
PIDFILE.unlink(missing_ok=True)
|
|
986
1078
|
sys.exit(1)
|
|
987
|
-
|
|
1079
|
+
_log_step(f"Daemon alive (pid {proc.pid})")
|
|
988
1080
|
if capture.get("browser", True):
|
|
989
1081
|
import urllib.request as _ur
|
|
990
1082
|
time.sleep(7)
|
|
@@ -1001,9 +1093,16 @@ def cmd_start(args: argparse.Namespace) -> None:
|
|
|
1001
1093
|
return
|
|
1002
1094
|
|
|
1003
1095
|
# Windows: foreground mode (no subprocess daemonization)
|
|
1096
|
+
_run_foreground(sid, watch_dir, cfg, capture, live_url, verbose=verbose, streaming=False)
|
|
1097
|
+
|
|
1098
|
+
|
|
1099
|
+
def _run_foreground(sid: str, watch_dir: str, cfg: dict, capture: dict,
|
|
1100
|
+
live_url: str, verbose: bool, streaming: bool) -> None:
|
|
1101
|
+
"""Run capture agents in the foreground (blocking). Used by --streaming and Windows."""
|
|
1004
1102
|
from methodproof.agents import base as _base
|
|
1005
|
-
_base.init(sid, live=bool(live_url))
|
|
1103
|
+
_base.init(sid, live=bool(live_url), verbose=verbose, streaming=streaming)
|
|
1006
1104
|
if capture.get("environment_analysis", True):
|
|
1105
|
+
_log_step("Scanning environment")
|
|
1007
1106
|
try:
|
|
1008
1107
|
from methodproof.analysis import scan_environment
|
|
1009
1108
|
env_profile = scan_environment(watch_dir)
|
|
@@ -1020,19 +1119,31 @@ def cmd_start(args: argparse.Namespace) -> None:
|
|
|
1020
1119
|
if files_enabled:
|
|
1021
1120
|
from methodproof.agents import watcher
|
|
1022
1121
|
threads.append(threading.Thread(target=watcher.start, args=(watch_dir, stop_event), daemon=True))
|
|
1122
|
+
_log_step("Agent: watcher")
|
|
1023
1123
|
if capture.get("terminal_commands", True) or capture.get("test_results", True):
|
|
1024
1124
|
from methodproof.agents import terminal
|
|
1025
1125
|
threads.append(threading.Thread(target=terminal.start, args=(stop_event,), daemon=True))
|
|
1126
|
+
_log_step("Agent: terminal")
|
|
1026
1127
|
if capture.get("browser", True):
|
|
1027
1128
|
from methodproof import bridge
|
|
1129
|
+
e2e_key_hex = ""
|
|
1130
|
+
if cfg.get("_session_e2e") and cfg.get("account_id"):
|
|
1131
|
+
from methodproof.keychain import load_secret
|
|
1132
|
+
_raw = load_secret(f"e2e:{cfg['account_id']}")
|
|
1133
|
+
if _raw:
|
|
1134
|
+
e2e_key_hex = _raw.hex()
|
|
1135
|
+
else:
|
|
1136
|
+
e2e_key_hex = cfg.get("e2e_key", "")
|
|
1028
1137
|
threads.append(threading.Thread(target=bridge.start, args=(
|
|
1029
1138
|
sid, stop_event, 9877,
|
|
1030
|
-
cfg.get("token", ""), cfg.get("api_url", ""),
|
|
1139
|
+
cfg.get("token", ""), cfg.get("api_url", ""), e2e_key_hex,
|
|
1031
1140
|
cfg.get("journal_mode", False),
|
|
1032
1141
|
), daemon=True))
|
|
1142
|
+
_log_step("Agent: bridge")
|
|
1033
1143
|
if capture.get("music", True):
|
|
1034
1144
|
from methodproof.agents import music
|
|
1035
1145
|
threads.append(threading.Thread(target=music.start, args=(stop_event,), daemon=True))
|
|
1146
|
+
_log_step("Agent: music")
|
|
1036
1147
|
|
|
1037
1148
|
def _shutdown(sig: int, frame: object) -> None:
|
|
1038
1149
|
stop_event.set()
|
|
@@ -1054,10 +1165,14 @@ def cmd_start(args: argparse.Namespace) -> None:
|
|
|
1054
1165
|
PIDFILE.unlink(missing_ok=True)
|
|
1055
1166
|
sys.exit(0)
|
|
1056
1167
|
|
|
1168
|
+
_log_step(f"Starting {len(threads)} agent(s)")
|
|
1057
1169
|
for t in threads:
|
|
1058
1170
|
t.start()
|
|
1059
1171
|
signal.signal(signal.SIGINT, _shutdown)
|
|
1060
|
-
|
|
1172
|
+
if streaming:
|
|
1173
|
+
print("\n Streaming events (Ctrl+C to stop):\n")
|
|
1174
|
+
else:
|
|
1175
|
+
print("Press Ctrl+C or run `mp stop` to finish.")
|
|
1061
1176
|
stopfile = config.DIR / "methodproof.stop"
|
|
1062
1177
|
while not stop_event.is_set():
|
|
1063
1178
|
if stopfile.exists():
|
|
@@ -1176,6 +1291,8 @@ def cmd_login(args: argparse.Namespace) -> None:
|
|
|
1176
1291
|
config.save(cfg)
|
|
1177
1292
|
print(" done.\n")
|
|
1178
1293
|
_setup_master_key(cfg)
|
|
1294
|
+
from methodproof.sync import sync_research_consent
|
|
1295
|
+
sync_research_consent(cfg["token"], cfg["api_url"])
|
|
1179
1296
|
print("Logged in. Run `methodproof push` to upload sessions.")
|
|
1180
1297
|
return
|
|
1181
1298
|
except Exception:
|
|
@@ -1190,6 +1307,9 @@ def cmd_push(args: argparse.Namespace) -> None:
|
|
|
1190
1307
|
if not cfg.get("token"):
|
|
1191
1308
|
print("Run `methodproof login` first.")
|
|
1192
1309
|
sys.exit(1)
|
|
1310
|
+
from methodproof.sync import sync_research_consent
|
|
1311
|
+
sync_research_consent(cfg["token"], cfg["api_url"])
|
|
1312
|
+
cfg = config.load()
|
|
1193
1313
|
sid = args.session_id or _latest()
|
|
1194
1314
|
if not sid:
|
|
1195
1315
|
print("No sessions to push.")
|
|
@@ -1583,6 +1703,10 @@ def main() -> None:
|
|
|
1583
1703
|
s.add_argument("--live", action="store_true", help="Stream graph live to your private profile")
|
|
1584
1704
|
s.add_argument("--live-public", action="store_true", help="Stream graph live — visible to anyone with the link")
|
|
1585
1705
|
s.add_argument("--journal", action="store_true", help="Journal mode — full content capture (2 free credits, then Pro)")
|
|
1706
|
+
s.add_argument("--e2e", action="store_true", help="E2E encryption — content encrypted with your personal key")
|
|
1707
|
+
s.add_argument("--no-e2e", action="store_true", help="Disable E2E for this session (overrides config)")
|
|
1708
|
+
s.add_argument("--verbose", "-v", action="store_true", help="Debug logging at each step (still daemonizes)")
|
|
1709
|
+
s.add_argument("--streaming", action="store_true", help="Blocking foreground — stream every captured event to stdout")
|
|
1586
1710
|
sub.add_parser("stop", help="Stop recording")
|
|
1587
1711
|
v = sub.add_parser("view", help="Inspect captured session data")
|
|
1588
1712
|
v.add_argument("session_id", nargs="?")
|
|
@@ -1626,6 +1750,14 @@ def main() -> None:
|
|
|
1626
1750
|
jr_sub.add_parser("on", help="Enable journal mode (persists full content)")
|
|
1627
1751
|
jr_sub.add_parser("off", help="Disable journal mode (structural only)")
|
|
1628
1752
|
jr_sub.add_parser("status", help="Show journal mode status")
|
|
1753
|
+
e2e_p = sub.add_parser("e2e", help="E2E encryption — personal key management")
|
|
1754
|
+
e2e_sub = e2e_p.add_subparsers(dest="e2e_cmd")
|
|
1755
|
+
e2e_sub.add_parser("on", help="Enable E2E mode (generates key on first use)")
|
|
1756
|
+
e2e_sub.add_parser("off", help="Disable E2E mode (key stays in keychain)")
|
|
1757
|
+
e2e_sub.add_parser("status", help="Show E2E mode status")
|
|
1758
|
+
e2e_sub.add_parser("recover", help="Recover key from passphrase")
|
|
1759
|
+
e2e_rel = e2e_sub.add_parser("release", help="Release a session from E2E encryption")
|
|
1760
|
+
e2e_rel.add_argument("session_id", help="Session ID to release")
|
|
1629
1761
|
sub.add_parser("intro", help="Show the MethodProof intro")
|
|
1630
1762
|
sub.add_parser("help", help="Show command reference")
|
|
1631
1763
|
sub.add_parser("mcp-serve", help="Run MCP server (used by Claude Code)")
|
|
@@ -1645,6 +1777,7 @@ def main() -> None:
|
|
|
1645
1777
|
"update": cmd_update, "lock": cmd_lock, "reset": cmd_reset, "uninstall": cmd_uninstall,
|
|
1646
1778
|
"extension": cmd_extension,
|
|
1647
1779
|
"journal": cmd_journal,
|
|
1780
|
+
"e2e": lambda a: __import__("methodproof.e2e", fromlist=["cmd_e2e"]).cmd_e2e(a),
|
|
1648
1781
|
"intro": lambda _: _print_intro(),
|
|
1649
1782
|
"help": lambda _: _print_commands(),
|
|
1650
1783
|
"mcp-serve": cmd_mcp_serve,
|
|
@@ -34,11 +34,16 @@ _DEFAULTS: dict[str, Any] = {
|
|
|
34
34
|
"code_capture": False,
|
|
35
35
|
},
|
|
36
36
|
"research_consent": False,
|
|
37
|
+
"contribution_level": None,
|
|
38
|
+
"_pending_research_sync": False,
|
|
37
39
|
"journal_mode": False,
|
|
38
40
|
"journal_credits": 2,
|
|
41
|
+
"e2e_mode": False,
|
|
42
|
+
"e2e_fingerprint": "",
|
|
39
43
|
"auto_update": False,
|
|
40
44
|
"account_id": "",
|
|
41
45
|
"last_auth_at": 0,
|
|
46
|
+
"local_ai_ports": [], # user-configured localhost ports for local LLM capture
|
|
42
47
|
"publish_redact": {
|
|
43
48
|
"command_output": True,
|
|
44
49
|
"ai_prompts": True,
|