ghostmode 0.1.0__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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 David Russell
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,157 @@
1
+ Metadata-Version: 2.4
2
+ Name: ghostmode
3
+ Version: 0.1.0
4
+ Summary: See what your AI agent would do, without letting it do anything. Zero-config dry-run proxy for MCP servers.
5
+ Author: David Russell
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/unwind-mcp/ghostmode
8
+ Project-URL: Repository, https://github.com/unwind-mcp/ghostmode
9
+ Project-URL: Full Security Suite, https://github.com/unwind-mcp/unwind
10
+ Keywords: mcp,ai-agents,ghost-mode,dry-run,proxy,safety
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Security
20
+ Requires-Python: >=3.9
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Provides-Extra: dev
24
+ Requires-Dist: pytest>=8.0; extra == "dev"
25
+ Dynamic: license-file
26
+
27
+ # Ghost Mode
28
+
29
+ **See what your AI agent would do, without letting it do anything.**
30
+
31
+ Ghost Mode is a safe first step before giving an AI agent real authority.
32
+
33
+ It's a zero-config dry-run layer for AI agents. It intercepts every write and lets reads through. Think of it as a flight simulator for AI agents — nothing gets modified, but you see everything the agent tried to do.
34
+
35
+ Requires Python 3.9+.
36
+
37
+ ## Quick Start
38
+
39
+ ### MCP Client Users (Claude Desktop, Cursor, etc.)
40
+
41
+ ```bash
42
+ pip install ghostmode
43
+ ghostmode -- npx @modelcontextprotocol/server-filesystem ~/Documents
44
+ ```
45
+
46
+ Point your agent at Ghost Mode instead of the real server. When the session ends, a summary is written to `ghostmode-summary.txt` in your working directory and printed to stderr.
47
+
48
+ ### OpenClaw Users
49
+
50
+ ```bash
51
+ # 1. Install Ghost Mode
52
+ pip install ghostmode
53
+
54
+ # 2. Install the OpenClaw plugin
55
+ openclaw plugins install @unwind/ghostmode-openclaw
56
+
57
+ # 3. Restart the gateway
58
+ ```
59
+
60
+ Ghost Mode hooks into OpenClaw's tool-call system. Every write is intercepted. Every read passes through.
61
+
62
+ ## What it does
63
+
64
+ Ghost Mode intercepts state-modifying tool calls — file writes, deletions, emails, API posts, shell commands — and returns fake success responses. The agent thinks it completed the action. Your filesystem, email, and APIs are untouched.
65
+
66
+ A shadow virtual filesystem keeps the agent consistent: if it "writes" a file and then reads it back, Ghost Mode serves the written content from memory. The agent doesn't know it's in a sandbox.
67
+
68
+ ## Example output
69
+
70
+ ```
71
+ =======================================================
72
+ GHOST MODE SESSION SUMMARY
73
+ =======================================================
74
+ Duration: 47.3s
75
+ Total events: 12
76
+ Intercepted: 4 writes blocked
77
+ Passed: 7 reads forwarded
78
+ Shadow reads: 1 served from VFS
79
+
80
+ Files the agent tried to write:
81
+ /home/user/Documents/report.md
82
+ /home/user/Documents/summary.txt
83
+
84
+ Files the agent tried to delete:
85
+ /home/user/Documents/old_draft.md
86
+
87
+ 4 write(s) were blocked. Nothing was modified.
88
+
89
+ Full timeline:
90
+ 14:23:01 [PASSED] fs_read /home/user/Documents/notes.txt
91
+ 14:23:02 [PASSED] fs_list /home/user/Documents
92
+ 14:23:03 [BLOCKED] fs_write /home/user/Documents/report.md
93
+ 14:23:04 [SHADOW] fs_read /home/user/Documents/report.md
94
+ 14:23:05 [BLOCKED] fs_delete /home/user/Documents/old_draft.md
95
+ 14:23:06 [BLOCKED] send_email
96
+ 14:23:07 [PASSED] fs_read /home/user/Documents/config.json
97
+ =======================================================
98
+ ```
99
+
100
+ ## Use cases
101
+
102
+ **Testing untrusted tools.** Install a new MCP server and see what it actually does before giving it real access.
103
+
104
+ **Reviewing agent behaviour.** Let your agent plan and "execute" a complex task, then review the timeline before running it for real.
105
+
106
+ **Demos and training.** Show stakeholders what an AI agent workflow looks like without risking real data.
107
+
108
+ **Compliance dry runs.** Verify that an agent workflow stays within policy boundaries before going live.
109
+
110
+ ## What Ghost Mode is not
111
+
112
+ Ghost Mode does not enforce policy or provide rollback. It is a visibility tool, not a security system. If you need enforcement, audit trails, or recovery, that's what [UNWIND](https://github.com/unwind-mcp/unwind) is for.
113
+
114
+ ## CLI options (MCP proxy mode)
115
+
116
+ ```
117
+ ghostmode -- <upstream command> Basic usage
118
+ ghostmode -v -- <command> Verbose: log every action to stderr
119
+ ghostmode --export log.json -- <cmd> Export session log on exit
120
+ ghostmode --also-block my_tool -- ... Add custom tool names to intercept
121
+ ```
122
+
123
+ ## How it knows what to block
124
+
125
+ Ghost Mode recognises 50+ common MCP tool names as state-modifying (filesystem writes, email sends, API posts, shell commands, database mutations, git operations). It also uses prefix heuristics: any tool starting with `create_`, `delete_`, `send_`, `write_`, `execute_`, etc. gets intercepted. The block list is heuristic-based and configurable.
126
+
127
+ If a tool slips through, use `--also-block tool_name` to add it.
128
+
129
+ ## Zero dependencies
130
+
131
+ Ghost Mode has no external dependencies. It uses only Python 3.9+ stdlib (asyncio, json, pathlib). It installs in under a second.
132
+
133
+ ## Known limitations
134
+
135
+ **Shell commands can't see ghost-written files.** If the agent writes a file through Ghost Mode and then tries to run it via `bash_exec` or `shell_exec`, the shell will fail because the file only exists in the in-memory shadow VFS, not on disk. Ghost Mode works best for tool-call-level dry runs, not multi-step build/execute workflows.
136
+
137
+ **Session summary in background clients.** Claude Desktop and Cursor run MCP servers in the background and may not surface stderr output. Ghost Mode automatically writes a `ghostmode-summary.txt` file to your working directory on exit. You can also use `--export log.json` for a machine-readable session log.
138
+
139
+ ## Want more?
140
+
141
+ Ghost Mode shows you what happened. **UNWIND** lets you control it:
142
+
143
+ - 15-stage enforcement pipeline (path jail, SSRF shield, DLP, canary honeypots)
144
+ - Smart snapshots with one-command rollback
145
+ - CRAFT tamper-evident audit chain
146
+ - Real-time trust light dashboard
147
+ - Detects encoded/obfuscated data exfiltration attempts
148
+
149
+ ```bash
150
+ pip install unwind-mcp
151
+ ```
152
+
153
+ [github.com/unwind-mcp/unwind](https://github.com/unwind-mcp/unwind)
154
+
155
+ ## License
156
+
157
+ MIT
@@ -0,0 +1,131 @@
1
+ # Ghost Mode
2
+
3
+ **See what your AI agent would do, without letting it do anything.**
4
+
5
+ Ghost Mode is a safe first step before giving an AI agent real authority.
6
+
7
+ It's a zero-config dry-run layer for AI agents. It intercepts every write and lets reads through. Think of it as a flight simulator for AI agents — nothing gets modified, but you see everything the agent tried to do.
8
+
9
+ Requires Python 3.9+.
10
+
11
+ ## Quick Start
12
+
13
+ ### MCP Client Users (Claude Desktop, Cursor, etc.)
14
+
15
+ ```bash
16
+ pip install ghostmode
17
+ ghostmode -- npx @modelcontextprotocol/server-filesystem ~/Documents
18
+ ```
19
+
20
+ Point your agent at Ghost Mode instead of the real server. When the session ends, a summary is written to `ghostmode-summary.txt` in your working directory and printed to stderr.
21
+
22
+ ### OpenClaw Users
23
+
24
+ ```bash
25
+ # 1. Install Ghost Mode
26
+ pip install ghostmode
27
+
28
+ # 2. Install the OpenClaw plugin
29
+ openclaw plugins install @unwind/ghostmode-openclaw
30
+
31
+ # 3. Restart the gateway
32
+ ```
33
+
34
+ Ghost Mode hooks into OpenClaw's tool-call system. Every write is intercepted. Every read passes through.
35
+
36
+ ## What it does
37
+
38
+ Ghost Mode intercepts state-modifying tool calls — file writes, deletions, emails, API posts, shell commands — and returns fake success responses. The agent thinks it completed the action. Your filesystem, email, and APIs are untouched.
39
+
40
+ A shadow virtual filesystem keeps the agent consistent: if it "writes" a file and then reads it back, Ghost Mode serves the written content from memory. The agent doesn't know it's in a sandbox.
41
+
42
+ ## Example output
43
+
44
+ ```
45
+ =======================================================
46
+ GHOST MODE SESSION SUMMARY
47
+ =======================================================
48
+ Duration: 47.3s
49
+ Total events: 12
50
+ Intercepted: 4 writes blocked
51
+ Passed: 7 reads forwarded
52
+ Shadow reads: 1 served from VFS
53
+
54
+ Files the agent tried to write:
55
+ /home/user/Documents/report.md
56
+ /home/user/Documents/summary.txt
57
+
58
+ Files the agent tried to delete:
59
+ /home/user/Documents/old_draft.md
60
+
61
+ 4 write(s) were blocked. Nothing was modified.
62
+
63
+ Full timeline:
64
+ 14:23:01 [PASSED] fs_read /home/user/Documents/notes.txt
65
+ 14:23:02 [PASSED] fs_list /home/user/Documents
66
+ 14:23:03 [BLOCKED] fs_write /home/user/Documents/report.md
67
+ 14:23:04 [SHADOW] fs_read /home/user/Documents/report.md
68
+ 14:23:05 [BLOCKED] fs_delete /home/user/Documents/old_draft.md
69
+ 14:23:06 [BLOCKED] send_email
70
+ 14:23:07 [PASSED] fs_read /home/user/Documents/config.json
71
+ =======================================================
72
+ ```
73
+
74
+ ## Use cases
75
+
76
+ **Testing untrusted tools.** Install a new MCP server and see what it actually does before giving it real access.
77
+
78
+ **Reviewing agent behaviour.** Let your agent plan and "execute" a complex task, then review the timeline before running it for real.
79
+
80
+ **Demos and training.** Show stakeholders what an AI agent workflow looks like without risking real data.
81
+
82
+ **Compliance dry runs.** Verify that an agent workflow stays within policy boundaries before going live.
83
+
84
+ ## What Ghost Mode is not
85
+
86
+ Ghost Mode does not enforce policy or provide rollback. It is a visibility tool, not a security system. If you need enforcement, audit trails, or recovery, that's what [UNWIND](https://github.com/unwind-mcp/unwind) is for.
87
+
88
+ ## CLI options (MCP proxy mode)
89
+
90
+ ```
91
+ ghostmode -- <upstream command> Basic usage
92
+ ghostmode -v -- <command> Verbose: log every action to stderr
93
+ ghostmode --export log.json -- <cmd> Export session log on exit
94
+ ghostmode --also-block my_tool -- ... Add custom tool names to intercept
95
+ ```
96
+
97
+ ## How it knows what to block
98
+
99
+ Ghost Mode recognises 50+ common MCP tool names as state-modifying (filesystem writes, email sends, API posts, shell commands, database mutations, git operations). It also uses prefix heuristics: any tool starting with `create_`, `delete_`, `send_`, `write_`, `execute_`, etc. gets intercepted. The block list is heuristic-based and configurable.
100
+
101
+ If a tool slips through, use `--also-block tool_name` to add it.
102
+
103
+ ## Zero dependencies
104
+
105
+ Ghost Mode has no external dependencies. It uses only Python 3.9+ stdlib (asyncio, json, pathlib). It installs in under a second.
106
+
107
+ ## Known limitations
108
+
109
+ **Shell commands can't see ghost-written files.** If the agent writes a file through Ghost Mode and then tries to run it via `bash_exec` or `shell_exec`, the shell will fail because the file only exists in the in-memory shadow VFS, not on disk. Ghost Mode works best for tool-call-level dry runs, not multi-step build/execute workflows.
110
+
111
+ **Session summary in background clients.** Claude Desktop and Cursor run MCP servers in the background and may not surface stderr output. Ghost Mode automatically writes a `ghostmode-summary.txt` file to your working directory on exit. You can also use `--export log.json` for a machine-readable session log.
112
+
113
+ ## Want more?
114
+
115
+ Ghost Mode shows you what happened. **UNWIND** lets you control it:
116
+
117
+ - 15-stage enforcement pipeline (path jail, SSRF shield, DLP, canary honeypots)
118
+ - Smart snapshots with one-command rollback
119
+ - CRAFT tamper-evident audit chain
120
+ - Real-time trust light dashboard
121
+ - Detects encoded/obfuscated data exfiltration attempts
122
+
123
+ ```bash
124
+ pip install unwind-mcp
125
+ ```
126
+
127
+ [github.com/unwind-mcp/unwind](https://github.com/unwind-mcp/unwind)
128
+
129
+ ## License
130
+
131
+ MIT
@@ -0,0 +1,6 @@
1
+ """Ghost Mode — dry-run proxy for AI agents.
2
+
3
+ See what your agent would do, before it does anything.
4
+ """
5
+
6
+ __version__ = "0.1.0"
@@ -0,0 +1,133 @@
1
+ from __future__ import annotations
2
+
3
+ """Ghost Mode CLI — one command, zero config.
4
+
5
+ Usage:
6
+ ghostmode -- npx @modelcontextprotocol/server-filesystem ~/Documents
7
+ ghostmode -v -- python my_mcp_server.py
8
+ ghostmode --export session.json -- node my_tools.js
9
+ """
10
+
11
+ import argparse
12
+ import asyncio
13
+ import logging
14
+ import sys
15
+
16
+
17
+ def main() -> None:
18
+ parser = argparse.ArgumentParser(
19
+ prog="ghostmode",
20
+ description=(
21
+ "\U0001f47b Ghost Mode — see what your AI agent would do, "
22
+ "without letting it do anything."
23
+ ),
24
+ epilog=(
25
+ "Example:\n"
26
+ " ghostmode -- npx @modelcontextprotocol/server-filesystem ~/Documents\n"
27
+ "\n"
28
+ "Point your agent at ghostmode instead of the MCP server.\n"
29
+ "Reads pass through. Writes get intercepted. Nothing changes.\n"
30
+ "\n"
31
+ "For full security (rollback, enforcement, audit chain), upgrade to UNWIND:\n"
32
+ " pip install unwind-mcp\n"
33
+ " https://github.com/unwind-mcp/unwind"
34
+ ),
35
+ formatter_class=argparse.RawDescriptionHelpFormatter,
36
+ )
37
+ parser.add_argument(
38
+ "upstream_command",
39
+ nargs=argparse.REMAINDER,
40
+ help="The MCP server command (after --)",
41
+ )
42
+ parser.add_argument(
43
+ "-v", "--verbose",
44
+ action="store_true",
45
+ help="Log every intercepted and passed action to stderr",
46
+ )
47
+ parser.add_argument(
48
+ "--export",
49
+ metavar="FILE",
50
+ help="Export the session log to a JSON file on exit",
51
+ )
52
+ parser.add_argument(
53
+ "--also-block",
54
+ metavar="TOOL",
55
+ action="append",
56
+ default=[],
57
+ help="Additional tool names to treat as writes (can be repeated)",
58
+ )
59
+
60
+ args = parser.parse_args()
61
+
62
+ # Parse upstream command
63
+ upstream_cmd = args.upstream_command
64
+ if upstream_cmd and upstream_cmd[0] == "--":
65
+ upstream_cmd = upstream_cmd[1:]
66
+
67
+ if not upstream_cmd:
68
+ print(
69
+ "\n \U0001f47b Ghost Mode — dry-run proxy for AI agents\n"
70
+ "\n"
71
+ " Usage:\n"
72
+ " ghostmode -- <upstream MCP server command>\n"
73
+ "\n"
74
+ " Examples:\n"
75
+ " ghostmode -- npx @modelcontextprotocol/server-filesystem ~/Documents\n"
76
+ " ghostmode -- python my_mcp_server.py\n"
77
+ " ghostmode -v -- node tools_server.js\n"
78
+ "\n"
79
+ " The agent talks to ghostmode. Reads pass through to your\n"
80
+ " real MCP server. Writes get intercepted. Nothing changes.\n"
81
+ "\n"
82
+ " When the session ends, you see everything the agent tried to do.\n",
83
+ file=sys.stderr,
84
+ )
85
+ sys.exit(1)
86
+
87
+ # Custom write tools
88
+ custom_writes = frozenset(args.also_block) if args.also_block else None
89
+
90
+ # Logging
91
+ level = logging.DEBUG if args.verbose else logging.WARNING
92
+ handler = logging.StreamHandler(sys.stderr)
93
+ handler.setFormatter(logging.Formatter(
94
+ "%(asctime)s [ghostmode] %(message)s", datefmt="%H:%M:%S",
95
+ ))
96
+ logging.root.addHandler(handler)
97
+ logging.root.setLevel(level)
98
+
99
+ # Banner
100
+ print(
101
+ "\n \U0001f47b Ghost Mode active\n"
102
+ f" Upstream: {' '.join(upstream_cmd)}\n"
103
+ f" Writes will be intercepted. Reads pass through.\n",
104
+ file=sys.stderr,
105
+ )
106
+
107
+ from .proxy import run_ghost_proxy
108
+
109
+ # Run with export on exit
110
+ async def _run():
111
+ from .proxy import GhostProxy
112
+ proxy = GhostProxy(upstream_cmd, custom_writes, args.verbose)
113
+ try:
114
+ await proxy.run()
115
+ except (KeyboardInterrupt, asyncio.CancelledError):
116
+ pass
117
+ finally:
118
+ await proxy.shutdown()
119
+
120
+ # Export if requested
121
+ if args.export:
122
+ from pathlib import Path
123
+ count = proxy.log.export_json(Path(args.export))
124
+ print(f"\n Exported {count} events to {args.export}", file=sys.stderr)
125
+
126
+ try:
127
+ asyncio.run(_run())
128
+ except KeyboardInterrupt:
129
+ pass
130
+
131
+
132
+ if __name__ == "__main__":
133
+ main()
@@ -0,0 +1,143 @@
1
+ from __future__ import annotations
2
+
3
+ """Ghost Mode event log — lightweight action recorder.
4
+
5
+ A minimal event log that records what the agent tried to do.
6
+ No SQLite, no hash chains — just an in-memory list with optional
7
+ file export. This is the "See Everything" without the security
8
+ infrastructure.
9
+
10
+ For the full flight recorder with CR-AFT hash chains, tamper
11
+ detection, and rollback, upgrade to UNWIND.
12
+ """
13
+
14
+ import json
15
+ import time
16
+ from dataclasses import dataclass, field, asdict
17
+ from pathlib import Path
18
+ from typing import Optional
19
+
20
+
21
+ @dataclass
22
+ class GhostEvent:
23
+ """A single intercepted action."""
24
+ timestamp: float
25
+ tool: str
26
+ action: str # "intercepted", "passed_through", "shadow_read"
27
+ target: Optional[str] = None
28
+ parameters_summary: Optional[str] = None # Brief summary, not full params
29
+ detail: Optional[str] = None
30
+
31
+
32
+ class GhostEventLog:
33
+ """In-memory event log for Ghost Mode sessions."""
34
+
35
+ def __init__(self):
36
+ self.events: list[GhostEvent] = []
37
+ self._start_time: float = time.time()
38
+
39
+ def log_intercept(self, tool: str, target: Optional[str] = None,
40
+ detail: Optional[str] = None) -> GhostEvent:
41
+ """Log an intercepted (blocked) write action."""
42
+ event = GhostEvent(
43
+ timestamp=time.time(),
44
+ tool=tool,
45
+ action="intercepted",
46
+ target=target,
47
+ detail=detail,
48
+ )
49
+ self.events.append(event)
50
+ return event
51
+
52
+ def log_passthrough(self, tool: str, target: Optional[str] = None) -> GhostEvent:
53
+ """Log a read-only action that was passed through."""
54
+ event = GhostEvent(
55
+ timestamp=time.time(),
56
+ tool=tool,
57
+ action="passed_through",
58
+ target=target,
59
+ )
60
+ self.events.append(event)
61
+ return event
62
+
63
+ def log_shadow_read(self, tool: str, target: str) -> GhostEvent:
64
+ """Log a read served from the shadow VFS."""
65
+ event = GhostEvent(
66
+ timestamp=time.time(),
67
+ tool=tool,
68
+ action="shadow_read",
69
+ target=target,
70
+ detail="Content served from shadow VFS (ghost-written earlier)",
71
+ )
72
+ self.events.append(event)
73
+ return event
74
+
75
+ @property
76
+ def intercepted_count(self) -> int:
77
+ return sum(1 for e in self.events if e.action == "intercepted")
78
+
79
+ @property
80
+ def passthrough_count(self) -> int:
81
+ return sum(1 for e in self.events if e.action == "passed_through")
82
+
83
+ @property
84
+ def shadow_read_count(self) -> int:
85
+ return sum(1 for e in self.events if e.action == "shadow_read")
86
+
87
+ @property
88
+ def duration_seconds(self) -> float:
89
+ return time.time() - self._start_time
90
+
91
+ def summary(self) -> dict:
92
+ """Return a session summary."""
93
+ return {
94
+ "duration_seconds": round(self.duration_seconds, 1),
95
+ "total_events": len(self.events),
96
+ "intercepted": self.intercepted_count,
97
+ "passed_through": self.passthrough_count,
98
+ "shadow_reads": self.shadow_read_count,
99
+ }
100
+
101
+ def format_timeline(self) -> str:
102
+ """Format events as a human-readable timeline."""
103
+ if not self.events:
104
+ return " No events recorded."
105
+
106
+ lines = []
107
+ for event in self.events:
108
+ ts = time.strftime("%H:%M:%S", time.localtime(event.timestamp))
109
+ target_str = f" → {event.target}" if event.target else ""
110
+
111
+ if event.action == "intercepted":
112
+ icon = "\U0001f6ab" # no entry
113
+ label = "BLOCKED"
114
+ elif event.action == "shadow_read":
115
+ icon = "\U0001f47b" # ghost
116
+ label = "SHADOW"
117
+ else:
118
+ icon = "\u2705" # check
119
+ label = "PASSED"
120
+
121
+ lines.append(f" {icon} {ts} [{label}] {event.tool}{target_str}")
122
+
123
+ if event.detail:
124
+ lines.append(f"{'':>14}\u2514\u2500 {event.detail}")
125
+
126
+ return "\n".join(lines)
127
+
128
+ def export_json(self, path: Path) -> int:
129
+ """Export the event log to a JSON file."""
130
+ data = {
131
+ "ghostmode_version": "0.1.0",
132
+ "session_summary": self.summary(),
133
+ "events": [asdict(e) for e in self.events],
134
+ }
135
+ path.write_text(json.dumps(data, indent=2))
136
+ return len(self.events)
137
+
138
+ def export_jsonl(self, path: Path) -> int:
139
+ """Export events as newline-delimited JSON."""
140
+ with open(path, "w") as f:
141
+ for event in self.events:
142
+ f.write(json.dumps(asdict(event)) + "\n")
143
+ return len(self.events)