note-connector 0.2.5 → 0.2.6
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.
- package/dist/paths.js +4 -0
- package/dist/setup-dependencies.js +56 -13
- package/package.json +3 -2
- package/py/pyproject.toml +86 -0
- package/py/src/note_mcp/__init__.py +7 -0
- package/py/src/note_mcp/__main__.py +65 -0
- package/py/src/note_mcp/api/__init__.py +31 -0
- package/py/src/note_mcp/api/articles.py +1395 -0
- package/py/src/note_mcp/api/client.py +318 -0
- package/py/src/note_mcp/api/embeds.py +482 -0
- package/py/src/note_mcp/api/images.py +456 -0
- package/py/src/note_mcp/api/preview.py +142 -0
- package/py/src/note_mcp/api/public_notes.py +150 -0
- package/py/src/note_mcp/auth/__init__.py +9 -0
- package/py/src/note_mcp/auth/browser.py +574 -0
- package/py/src/note_mcp/auth/file_session.py +145 -0
- package/py/src/note_mcp/auth/session.py +240 -0
- package/py/src/note_mcp/browser/__init__.py +10 -0
- package/py/src/note_mcp/browser/config.py +21 -0
- package/py/src/note_mcp/browser/manager.py +182 -0
- package/py/src/note_mcp/browser/preview.py +68 -0
- package/py/src/note_mcp/browser/url_helpers.py +18 -0
- package/py/src/note_mcp/chatgpt/__init__.py +1 -0
- package/py/src/note_mcp/chatgpt/__main__.py +63 -0
- package/py/src/note_mcp/chatgpt/access_log.py +25 -0
- package/py/src/note_mcp/chatgpt/auth.py +52 -0
- package/py/src/note_mcp/chatgpt/images.py +92 -0
- package/py/src/note_mcp/chatgpt/login_once.py +26 -0
- package/py/src/note_mcp/chatgpt/middleware.py +31 -0
- package/py/src/note_mcp/chatgpt/tools.py +255 -0
- package/py/src/note_mcp/chatgpt/widgets.py +121 -0
- package/py/src/note_mcp/decorators.py +113 -0
- package/py/src/note_mcp/investigator/__init__.py +33 -0
- package/py/src/note_mcp/investigator/__main__.py +11 -0
- package/py/src/note_mcp/investigator/cli.py +313 -0
- package/py/src/note_mcp/investigator/core.py +653 -0
- package/py/src/note_mcp/investigator/mcp_tools.py +225 -0
- package/py/src/note_mcp/models.py +557 -0
- package/py/src/note_mcp/py.typed +0 -0
- package/py/src/note_mcp/server.py +905 -0
- package/py/src/note_mcp/utils/__init__.py +7 -0
- package/py/src/note_mcp/utils/file_parser.py +314 -0
- package/py/src/note_mcp/utils/html_to_markdown.py +477 -0
- package/py/src/note_mcp/utils/logging.py +119 -0
- package/py/src/note_mcp/utils/markdown.py +12 -0
- package/py/src/note_mcp/utils/markdown_to_html.py +826 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Decorators for MCP tool handlers.
|
|
2
|
+
|
|
3
|
+
Provides common functionality for MCP tool handlers:
|
|
4
|
+
- Session validation
|
|
5
|
+
- API error handling
|
|
6
|
+
|
|
7
|
+
Decorator Order:
|
|
8
|
+
When combining decorators, apply in this order (outermost first):
|
|
9
|
+
|
|
10
|
+
@handle_api_error # Catches NoteAPIError from the inner function
|
|
11
|
+
@require_session # Validates session before calling handler
|
|
12
|
+
async def handler(session: Session, ...) -> str:
|
|
13
|
+
...
|
|
14
|
+
|
|
15
|
+
This ensures session validation happens first, then API errors
|
|
16
|
+
are caught and formatted.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import inspect
|
|
22
|
+
import logging
|
|
23
|
+
from collections.abc import Awaitable, Callable
|
|
24
|
+
from functools import wraps
|
|
25
|
+
from typing import TYPE_CHECKING, Concatenate
|
|
26
|
+
|
|
27
|
+
from note_mcp.auth.session import SessionManager
|
|
28
|
+
from note_mcp.models import NoteAPIError
|
|
29
|
+
|
|
30
|
+
if TYPE_CHECKING:
|
|
31
|
+
from note_mcp.models import Session
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
# Session manager instance for decorators
|
|
36
|
+
# (SessionManager uses persistent storage, so multiple instances are safe)
|
|
37
|
+
_session_manager = SessionManager()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def require_session[**P](
|
|
41
|
+
func: Callable[Concatenate[Session, P], Awaitable[str]],
|
|
42
|
+
) -> Callable[P, Awaitable[str]]:
|
|
43
|
+
"""Decorator to validate session before executing handler.
|
|
44
|
+
|
|
45
|
+
Loads the session from SessionManager. If the session is valid,
|
|
46
|
+
it is passed as the first argument to the decorated function.
|
|
47
|
+
If the session is invalid or expired, returns an error message.
|
|
48
|
+
|
|
49
|
+
Usage:
|
|
50
|
+
@require_session
|
|
51
|
+
async def my_handler(session: Session, arg1: str) -> str:
|
|
52
|
+
# session is automatically injected
|
|
53
|
+
return await do_something(session, arg1)
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
func: The async function to wrap. Must accept Session as first argument.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
Wrapped function that validates session before calling the original.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
@wraps(func)
|
|
63
|
+
async def wrapper(*args: P.args, **kwargs: P.kwargs) -> str:
|
|
64
|
+
session = _session_manager.load()
|
|
65
|
+
if session is None or session.is_expired():
|
|
66
|
+
return "セッションが無効です。note_loginでログインしてください。"
|
|
67
|
+
return await func(session, *args, **kwargs)
|
|
68
|
+
|
|
69
|
+
# Remove 'session' parameter from the signature so it's not exposed in MCP schema
|
|
70
|
+
# See: https://github.com/drillan/note-mcp/issues/238
|
|
71
|
+
original_sig = inspect.signature(func)
|
|
72
|
+
new_params = [param for name, param in original_sig.parameters.items() if name != "session"]
|
|
73
|
+
wrapper.__signature__ = original_sig.replace(parameters=new_params) # type: ignore[attr-defined]
|
|
74
|
+
|
|
75
|
+
return wrapper
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def handle_api_error[**P](
|
|
79
|
+
func: Callable[P, Awaitable[str]],
|
|
80
|
+
) -> Callable[P, Awaitable[str]]:
|
|
81
|
+
"""Decorator to catch and format NoteAPIError exceptions.
|
|
82
|
+
|
|
83
|
+
Wraps the function in a try-except block. If NoteAPIError is raised,
|
|
84
|
+
returns a formatted error message. Other exceptions are propagated.
|
|
85
|
+
|
|
86
|
+
Usage:
|
|
87
|
+
@handle_api_error
|
|
88
|
+
async def my_handler(session: Session, arg1: str) -> str:
|
|
89
|
+
result = await api_call(session, arg1)
|
|
90
|
+
return f"Success: {result}"
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
func: The async function to wrap.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Wrapped function that catches NoteAPIError exceptions.
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
@wraps(func)
|
|
100
|
+
async def wrapper(*args: P.args, **kwargs: P.kwargs) -> str:
|
|
101
|
+
try:
|
|
102
|
+
return await func(*args, **kwargs)
|
|
103
|
+
except NoteAPIError as e:
|
|
104
|
+
logger.error(
|
|
105
|
+
"NoteAPIError in %s: code=%s, message=%s",
|
|
106
|
+
func.__name__,
|
|
107
|
+
e.code.value,
|
|
108
|
+
e.message,
|
|
109
|
+
exc_info=True,
|
|
110
|
+
)
|
|
111
|
+
return f"エラー [{e.code.value}]: {e.message}"
|
|
112
|
+
|
|
113
|
+
return wrapper
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""HTTP traffic investigation module for note.com API analysis.
|
|
2
|
+
|
|
3
|
+
This module provides tools for capturing and analyzing HTTP traffic
|
|
4
|
+
to understand note.com API behavior.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
# Interactive capture
|
|
8
|
+
uv run python -m note_mcp.investigator capture
|
|
9
|
+
|
|
10
|
+
# Analyze captured traffic
|
|
11
|
+
uv run python -m note_mcp.investigator analyze traffic.flow
|
|
12
|
+
|
|
13
|
+
# Export to JSON
|
|
14
|
+
uv run python -m note_mcp.investigator export traffic.flow
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from note_mcp.investigator.core import (
|
|
18
|
+
CapturedRequest,
|
|
19
|
+
CaptureSession,
|
|
20
|
+
CaptureSessionManager,
|
|
21
|
+
ProxyManager,
|
|
22
|
+
run_capture_session,
|
|
23
|
+
)
|
|
24
|
+
from note_mcp.investigator.mcp_tools import register_investigator_tools
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"CapturedRequest",
|
|
28
|
+
"CaptureSession",
|
|
29
|
+
"CaptureSessionManager",
|
|
30
|
+
"ProxyManager",
|
|
31
|
+
"register_investigator_tools",
|
|
32
|
+
"run_capture_session",
|
|
33
|
+
]
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Entry point for running investigator as a module.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
uv run python -m note_mcp.investigator capture
|
|
5
|
+
uv run python -m note_mcp.investigator analyze traffic.flow
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from note_mcp.investigator.cli import main
|
|
9
|
+
|
|
10
|
+
if __name__ == "__main__":
|
|
11
|
+
main()
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
"""CLI interface for HTTP traffic investigation.
|
|
2
|
+
|
|
3
|
+
Provides commands for capturing and analyzing HTTP traffic
|
|
4
|
+
to investigate note.com API behavior.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import re
|
|
11
|
+
import subprocess
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Annotated
|
|
14
|
+
|
|
15
|
+
import typer
|
|
16
|
+
|
|
17
|
+
app = typer.Typer(
|
|
18
|
+
name="investigator",
|
|
19
|
+
help="note.com API investigation tool - capture and analyze HTTP traffic",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@app.command()
|
|
24
|
+
def capture(
|
|
25
|
+
output: Annotated[
|
|
26
|
+
Path,
|
|
27
|
+
typer.Option(
|
|
28
|
+
"--output",
|
|
29
|
+
"-o",
|
|
30
|
+
help="Output file for captured traffic (.flow format)",
|
|
31
|
+
),
|
|
32
|
+
] = Path("traffic.flow"),
|
|
33
|
+
url: Annotated[
|
|
34
|
+
str,
|
|
35
|
+
typer.Option(
|
|
36
|
+
"--url",
|
|
37
|
+
"-u",
|
|
38
|
+
help="Initial URL to navigate to",
|
|
39
|
+
),
|
|
40
|
+
] = "https://note.com",
|
|
41
|
+
port: Annotated[
|
|
42
|
+
int,
|
|
43
|
+
typer.Option(
|
|
44
|
+
"--port",
|
|
45
|
+
"-p",
|
|
46
|
+
help="Proxy server port",
|
|
47
|
+
),
|
|
48
|
+
] = 8080,
|
|
49
|
+
domain: Annotated[
|
|
50
|
+
str | None,
|
|
51
|
+
typer.Option(
|
|
52
|
+
"--domain",
|
|
53
|
+
"-d",
|
|
54
|
+
help="Filter traffic by domain (e.g., 'api.note.com')",
|
|
55
|
+
),
|
|
56
|
+
] = None,
|
|
57
|
+
no_session: Annotated[
|
|
58
|
+
bool,
|
|
59
|
+
typer.Option(
|
|
60
|
+
"--no-session",
|
|
61
|
+
help="Don't restore saved session (require fresh login)",
|
|
62
|
+
),
|
|
63
|
+
] = False,
|
|
64
|
+
) -> None:
|
|
65
|
+
"""Start interactive traffic capture session.
|
|
66
|
+
|
|
67
|
+
Opens a browser with proxy configured to capture all HTTP traffic.
|
|
68
|
+
By default, restores saved session cookies from note_login.
|
|
69
|
+
Perform your investigation in the browser, then close it to save the capture.
|
|
70
|
+
|
|
71
|
+
Example:
|
|
72
|
+
uv run python -m note_mcp.investigator capture --domain api.note.com
|
|
73
|
+
"""
|
|
74
|
+
from note_mcp.investigator.core import run_capture_session
|
|
75
|
+
|
|
76
|
+
typer.echo("🔍 Starting traffic capture...")
|
|
77
|
+
typer.echo(f" Output: {output}")
|
|
78
|
+
typer.echo(f" Proxy port: {port}")
|
|
79
|
+
if domain:
|
|
80
|
+
typer.echo(f" Domain filter: {domain}")
|
|
81
|
+
typer.echo(f" Session restore: {'disabled' if no_session else 'enabled'}")
|
|
82
|
+
typer.echo()
|
|
83
|
+
typer.echo("📋 Instructions:")
|
|
84
|
+
typer.echo(" 1. Browser will open with proxy configured")
|
|
85
|
+
if not no_session:
|
|
86
|
+
typer.echo(" 2. If logged in, session will be restored automatically")
|
|
87
|
+
typer.echo(" 3. If auto-navigation fails, manually enter the URL")
|
|
88
|
+
typer.echo(" 4. Perform the actions you want to investigate")
|
|
89
|
+
typer.echo(" 5. Close the browser when done")
|
|
90
|
+
typer.echo()
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
asyncio.run(
|
|
94
|
+
run_capture_session(
|
|
95
|
+
output=output,
|
|
96
|
+
initial_url=url,
|
|
97
|
+
proxy_port=port,
|
|
98
|
+
domain_filter=domain,
|
|
99
|
+
restore_session=not no_session,
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
typer.echo(f"✅ Capture saved to: {output}")
|
|
103
|
+
typer.echo()
|
|
104
|
+
typer.echo("Next steps:")
|
|
105
|
+
typer.echo(f" uv run python -m note_mcp.investigator analyze {output}")
|
|
106
|
+
|
|
107
|
+
except RuntimeError as e:
|
|
108
|
+
typer.echo(f"❌ Error: {e}", err=True)
|
|
109
|
+
raise typer.Exit(1) from e
|
|
110
|
+
except KeyboardInterrupt:
|
|
111
|
+
typer.echo("\n⚠️ Capture interrupted")
|
|
112
|
+
raise typer.Exit(0) from None
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@app.command()
|
|
116
|
+
def analyze(
|
|
117
|
+
file: Annotated[
|
|
118
|
+
Path,
|
|
119
|
+
typer.Argument(
|
|
120
|
+
help="Traffic capture file to analyze (.flow format)",
|
|
121
|
+
),
|
|
122
|
+
],
|
|
123
|
+
pattern: Annotated[
|
|
124
|
+
str | None,
|
|
125
|
+
typer.Option(
|
|
126
|
+
"--pattern",
|
|
127
|
+
"-p",
|
|
128
|
+
help="Regex pattern to search in requests/responses",
|
|
129
|
+
),
|
|
130
|
+
] = None,
|
|
131
|
+
domain: Annotated[
|
|
132
|
+
str | None,
|
|
133
|
+
typer.Option(
|
|
134
|
+
"--domain",
|
|
135
|
+
"-d",
|
|
136
|
+
help="Filter by domain",
|
|
137
|
+
),
|
|
138
|
+
] = None,
|
|
139
|
+
method: Annotated[
|
|
140
|
+
str | None,
|
|
141
|
+
typer.Option(
|
|
142
|
+
"--method",
|
|
143
|
+
"-m",
|
|
144
|
+
help="Filter by HTTP method (GET, POST, PUT, etc.)",
|
|
145
|
+
),
|
|
146
|
+
] = None,
|
|
147
|
+
show_body: Annotated[
|
|
148
|
+
bool,
|
|
149
|
+
typer.Option(
|
|
150
|
+
"--body",
|
|
151
|
+
"-b",
|
|
152
|
+
help="Show request/response bodies",
|
|
153
|
+
),
|
|
154
|
+
] = False,
|
|
155
|
+
) -> None:
|
|
156
|
+
"""Analyze captured traffic file.
|
|
157
|
+
|
|
158
|
+
Reads a .flow file captured by mitmproxy and displays matching requests.
|
|
159
|
+
|
|
160
|
+
Example:
|
|
161
|
+
uv run python -m note_mcp.investigator analyze traffic.flow --pattern citation
|
|
162
|
+
"""
|
|
163
|
+
if not file.exists():
|
|
164
|
+
typer.echo(f"❌ File not found: {file}", err=True)
|
|
165
|
+
raise typer.Exit(1)
|
|
166
|
+
|
|
167
|
+
typer.echo(f"📊 Analyzing: {file}")
|
|
168
|
+
if pattern:
|
|
169
|
+
typer.echo(f" Pattern: {pattern}")
|
|
170
|
+
if domain:
|
|
171
|
+
typer.echo(f" Domain: {domain}")
|
|
172
|
+
if method:
|
|
173
|
+
typer.echo(f" Method: {method}")
|
|
174
|
+
typer.echo()
|
|
175
|
+
|
|
176
|
+
# Build mitmdump filter
|
|
177
|
+
filters: list[str] = []
|
|
178
|
+
if domain:
|
|
179
|
+
filters.append(f"~d {domain}")
|
|
180
|
+
if method:
|
|
181
|
+
filters.append(f"~m {method.upper()}")
|
|
182
|
+
|
|
183
|
+
# Use mitmdump to read and display the flow
|
|
184
|
+
cmd = ["mitmdump", "-n", "-r", str(file)]
|
|
185
|
+
if filters:
|
|
186
|
+
cmd.extend(["--set", f"flow_filter={'&'.join(filters)}"])
|
|
187
|
+
|
|
188
|
+
try:
|
|
189
|
+
result = subprocess.run(
|
|
190
|
+
cmd,
|
|
191
|
+
capture_output=True,
|
|
192
|
+
text=True,
|
|
193
|
+
check=False,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
output_lines = result.stdout.strip().split("\n") if result.stdout else []
|
|
197
|
+
|
|
198
|
+
if pattern:
|
|
199
|
+
# Filter lines by pattern
|
|
200
|
+
regex = re.compile(pattern, re.IGNORECASE)
|
|
201
|
+
matched_lines: list[str] = []
|
|
202
|
+
for line in output_lines:
|
|
203
|
+
if regex.search(line):
|
|
204
|
+
matched_lines.append(line)
|
|
205
|
+
output_lines = matched_lines
|
|
206
|
+
|
|
207
|
+
if not output_lines or (len(output_lines) == 1 and not output_lines[0]):
|
|
208
|
+
typer.echo("No matching requests found.")
|
|
209
|
+
return
|
|
210
|
+
|
|
211
|
+
typer.echo(f"Found {len(output_lines)} matching request(s):\n")
|
|
212
|
+
for line in output_lines:
|
|
213
|
+
typer.echo(line)
|
|
214
|
+
|
|
215
|
+
if result.stderr:
|
|
216
|
+
typer.echo(f"\n⚠️ Warnings: {result.stderr}", err=True)
|
|
217
|
+
|
|
218
|
+
except FileNotFoundError:
|
|
219
|
+
typer.echo("❌ mitmdump not found. Install mitmproxy first.", err=True)
|
|
220
|
+
raise typer.Exit(1) from None
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
@app.command()
|
|
224
|
+
def export(
|
|
225
|
+
file: Annotated[
|
|
226
|
+
Path,
|
|
227
|
+
typer.Argument(
|
|
228
|
+
help="Traffic capture file to export (.flow format)",
|
|
229
|
+
),
|
|
230
|
+
],
|
|
231
|
+
output: Annotated[
|
|
232
|
+
Path,
|
|
233
|
+
typer.Option(
|
|
234
|
+
"--output",
|
|
235
|
+
"-o",
|
|
236
|
+
help="Output file (JSON format)",
|
|
237
|
+
),
|
|
238
|
+
] = Path("traffic.json"),
|
|
239
|
+
domain: Annotated[
|
|
240
|
+
str | None,
|
|
241
|
+
typer.Option(
|
|
242
|
+
"--domain",
|
|
243
|
+
"-d",
|
|
244
|
+
help="Filter by domain",
|
|
245
|
+
),
|
|
246
|
+
] = None,
|
|
247
|
+
) -> None:
|
|
248
|
+
"""Export captured traffic to JSON format.
|
|
249
|
+
|
|
250
|
+
Converts mitmproxy .flow format to readable JSON for further analysis.
|
|
251
|
+
|
|
252
|
+
Example:
|
|
253
|
+
uv run python -m note_mcp.investigator export traffic.flow -o api_calls.json
|
|
254
|
+
"""
|
|
255
|
+
if not file.exists():
|
|
256
|
+
typer.echo(f"❌ File not found: {file}", err=True)
|
|
257
|
+
raise typer.Exit(1)
|
|
258
|
+
|
|
259
|
+
typer.echo(f"📤 Exporting: {file} -> {output}")
|
|
260
|
+
|
|
261
|
+
# Use mitmproxy Python API to read flows
|
|
262
|
+
try:
|
|
263
|
+
from mitmproxy import io as mio
|
|
264
|
+
from mitmproxy.http import HTTPFlow
|
|
265
|
+
except ImportError:
|
|
266
|
+
typer.echo("❌ mitmproxy not installed. Run: uv sync --group dev", err=True)
|
|
267
|
+
raise typer.Exit(1) from None
|
|
268
|
+
|
|
269
|
+
import json
|
|
270
|
+
|
|
271
|
+
flows_data: list[dict[str, object]] = []
|
|
272
|
+
|
|
273
|
+
with open(file, "rb") as f:
|
|
274
|
+
reader = mio.FlowReader(f)
|
|
275
|
+
for flow in reader.stream():
|
|
276
|
+
if not isinstance(flow, HTTPFlow):
|
|
277
|
+
continue
|
|
278
|
+
|
|
279
|
+
# Apply domain filter
|
|
280
|
+
if domain and domain not in flow.request.host:
|
|
281
|
+
continue
|
|
282
|
+
|
|
283
|
+
flow_data: dict[str, object] = {
|
|
284
|
+
"request": {
|
|
285
|
+
"method": flow.request.method,
|
|
286
|
+
"url": flow.request.url,
|
|
287
|
+
"headers": dict(flow.request.headers),
|
|
288
|
+
"body": flow.request.get_text(strict=False),
|
|
289
|
+
},
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if flow.response:
|
|
293
|
+
flow_data["response"] = {
|
|
294
|
+
"status_code": flow.response.status_code,
|
|
295
|
+
"headers": dict(flow.response.headers),
|
|
296
|
+
"body": flow.response.get_text(strict=False),
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
flows_data.append(flow_data)
|
|
300
|
+
|
|
301
|
+
with open(output, "w", encoding="utf-8") as f:
|
|
302
|
+
json.dump(flows_data, f, indent=2, ensure_ascii=False)
|
|
303
|
+
|
|
304
|
+
typer.echo(f"✅ Exported {len(flows_data)} request(s) to: {output}")
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def main() -> None:
|
|
308
|
+
"""Entry point for the CLI."""
|
|
309
|
+
app()
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
if __name__ == "__main__":
|
|
313
|
+
main()
|