cli-web-chatgpt 0.1.0__py3-none-any.whl

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,87 @@
1
+ # cli-web-chatgpt
2
+
3
+ CLI for ChatGPT web interface — ask questions, generate images, download images, manage conversations.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ cd chatgpt/agent-harness
9
+ pip install -e ".[auth]"
10
+ playwright install chromium
11
+ ```
12
+
13
+ ## Authentication
14
+
15
+ The CLI uses browser-based authentication. First capture your session:
16
+
17
+ ```bash
18
+ # Option 1: Use playwright-cli to login and save state
19
+ npx @playwright/cli@latest -s=chatgpt open https://chatgpt.com/ --headed --persistent
20
+ # Log in, then:
21
+ npx @playwright/cli@latest -s=chatgpt state-save chatgpt/traffic-capture/fresh-auth.json
22
+ npx @playwright/cli@latest -s=chatgpt close
23
+
24
+ # Option 2: Extract token from cookies
25
+ # The CLI auto-extracts access tokens from saved browser state
26
+ ```
27
+
28
+ Check auth status:
29
+ ```bash
30
+ cli-web-chatgpt auth status --json
31
+ ```
32
+
33
+ ## Usage
34
+
35
+ ### Ask a question
36
+ ```bash
37
+ cli-web-chatgpt chat ask "What is the capital of France?"
38
+ cli-web-chatgpt chat ask "Explain quantum computing" --model gpt-5-3
39
+ cli-web-chatgpt chat ask "What is 2+2?" --json
40
+ ```
41
+
42
+ ### Generate an image
43
+ ```bash
44
+ cli-web-chatgpt chat image "A cat wearing a hat"
45
+ cli-web-chatgpt chat image "Sunset over mountains" -o sunset.png
46
+ cli-web-chatgpt chat image "Abstract art" --json
47
+ ```
48
+
49
+ ### List conversations
50
+ ```bash
51
+ cli-web-chatgpt conversations list
52
+ cli-web-chatgpt conversations list --limit 5
53
+ cli-web-chatgpt conversations list --archived --json
54
+ ```
55
+
56
+ ### Browse generated images
57
+ ```bash
58
+ cli-web-chatgpt images list
59
+ cli-web-chatgpt images download <file_id> -c <conversation_id> -o image.png
60
+ cli-web-chatgpt images styles
61
+ ```
62
+
63
+ ### Account info
64
+ ```bash
65
+ cli-web-chatgpt me
66
+ cli-web-chatgpt models
67
+ ```
68
+
69
+ ### REPL mode
70
+ ```bash
71
+ cli-web-chatgpt # Enters interactive REPL
72
+ cli-web-chatgpt --json # REPL with JSON output
73
+ ```
74
+
75
+ ## Architecture
76
+
77
+ - **Read-only endpoints** (conversations, models, me, images): `curl_cffi` with Chrome TLS impersonation
78
+ - **Chat/image generation**: Playwright headless browser (handles Cloudflare + sentinel anti-abuse)
79
+ - **Auth**: Browser cookies + JWT access token from `/api/auth/session`
80
+
81
+ ## JSON Output
82
+
83
+ All commands support `--json` for structured output:
84
+ ```json
85
+ {"success": true, "data": {...}}
86
+ {"error": true, "code": "AUTH_EXPIRED", "message": "..."}
87
+ ```
@@ -0,0 +1,3 @@
1
+ """cli-web-chatgpt — CLI for ChatGPT web interface."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,5 @@
1
+ """Allow running as: python -m cli_web.chatgpt"""
2
+
3
+ from .chatgpt_cli import main
4
+
5
+ main()
@@ -0,0 +1,132 @@
1
+ """cli-web-chatgpt — CLI for ChatGPT web interface."""
2
+
3
+ import sys
4
+
5
+ if sys.stdout and sys.stdout.encoding and sys.stdout.encoding.lower() not in ("utf-8", "utf8"):
6
+ try:
7
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
8
+ except AttributeError:
9
+ pass
10
+ if sys.stderr and sys.stderr.encoding and sys.stderr.encoding.lower() not in ("utf-8", "utf8"):
11
+ try:
12
+ sys.stderr.reconfigure(encoding="utf-8", errors="replace")
13
+ except AttributeError:
14
+ pass
15
+
16
+ import shlex
17
+
18
+ import click
19
+
20
+ from .commands.account import me, models
21
+ from .commands.auth_cmd import auth_group
22
+ from .commands.chat import chat_group
23
+ from .commands.conversations import conversations_group
24
+ from .commands.images import images_group
25
+ from .utils.repl_skin import ReplSkin
26
+
27
+ _skin = ReplSkin("chatgpt", version="0.1.0")
28
+
29
+
30
+ @click.group(invoke_without_command=True)
31
+ @click.version_option("0.1.0", prog_name="cli-web-chatgpt")
32
+ @click.option("--json", "json_mode", is_flag=True, help="Output as JSON.")
33
+ @click.pass_context
34
+ def cli(ctx, json_mode: bool) -> None:
35
+ """CLI for ChatGPT — ask questions, generate images, manage conversations."""
36
+ ctx.ensure_object(dict)
37
+ ctx.obj["json"] = json_mode
38
+
39
+ if ctx.invoked_subcommand is None:
40
+ _run_repl(ctx)
41
+
42
+
43
+ # Register command groups and standalone commands
44
+ cli.add_command(chat_group)
45
+ cli.add_command(conversations_group)
46
+ cli.add_command(images_group)
47
+ cli.add_command(auth_group)
48
+ cli.add_command(me)
49
+ cli.add_command(models)
50
+
51
+
52
+ def _print_repl_help() -> None:
53
+ _skin.info("Available commands:")
54
+ print(" chat ask <question> Ask ChatGPT a question")
55
+ print(" --model <slug> Model (e.g. gpt-5-4-thinking)")
56
+ print(" --conversation <id> Continue existing conversation")
57
+ print(" chat image <prompt> Generate an image")
58
+ print(" --style <name> Apply a style")
59
+ print(" --output <path> Save image to file")
60
+ print(" --conversation <id> Continue existing conversation")
61
+ print(" conversations list List recent conversations")
62
+ print(" --limit <n> Number to show (default: 20)")
63
+ print(" --archived / --starred Filter")
64
+ print(" conversations get <id> View a conversation")
65
+ print(" images list List recently generated images")
66
+ print(" images download <file_id> Download a generated image")
67
+ print(" --conversation <id> (required) Conversation containing the image")
68
+ print(" --output <path> Output file path")
69
+ print(" images styles List available image styles")
70
+ print(" models List available models")
71
+ print(" me Show current user info")
72
+ print(" auth login Login via browser")
73
+ print(" auth status Check auth status")
74
+ print(" auth logout Remove stored credentials")
75
+ print()
76
+ print(" help Show this help")
77
+ print(" exit / quit Exit REPL")
78
+
79
+
80
+ def _run_repl(ctx) -> None:
81
+ _skin.print_banner()
82
+
83
+ while True:
84
+ try:
85
+ line = input(_skin.prompt()).strip()
86
+ except (EOFError, KeyboardInterrupt):
87
+ _skin.print_goodbye()
88
+ break
89
+
90
+ if not line:
91
+ continue
92
+ if line.lower() in ("exit", "quit", "q"):
93
+ _skin.print_goodbye()
94
+ break
95
+ if line.lower() in ("help", "?"):
96
+ _print_repl_help()
97
+ continue
98
+
99
+ try:
100
+ args = shlex.split(line)
101
+ except ValueError as exc:
102
+ _skin.error(f"Parse error: {exc}")
103
+ continue
104
+
105
+ repl_args = ["--json"] + args if ctx.obj.get("json") else args
106
+
107
+ try:
108
+ cli.main(args=repl_args, standalone_mode=False)
109
+ except SystemExit:
110
+ pass
111
+ except click.exceptions.UsageError as exc:
112
+ _skin.error(str(exc))
113
+ except Exception as exc:
114
+ _skin.error(f"{type(exc).__name__}: {exc}")
115
+
116
+
117
+ def main():
118
+ cli()
119
+
120
+
121
+ # MCP server mode — exposes every command as an MCP tool over stdio.
122
+ # Canonical adapter: cli-web-core/cli_web_core/mcp_server.py (vendored copy).
123
+ from cli_web.chatgpt import __version__ as _pkg_version # noqa: E402
124
+ from cli_web.chatgpt.utils.doctor import register_doctor_command # noqa: E402
125
+ from cli_web.chatgpt.utils.mcp_server import register_mcp_command # noqa: E402
126
+
127
+ register_mcp_command(cli, app_name="chatgpt", version=_pkg_version)
128
+ register_doctor_command(cli, app_name="chatgpt", pkg="chatgpt")
129
+
130
+
131
+ if __name__ == "__main__":
132
+ main()
File without changes
@@ -0,0 +1,57 @@
1
+ """Account and model commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import click
6
+
7
+ from ..core.client import ChatGPTClient
8
+ from ..utils.helpers import handle_errors, resolve_json_mode
9
+ from ..utils.output import print_json
10
+
11
+
12
+ @click.command("me")
13
+ @click.option("--json", "json_mode", is_flag=True, help="Output as JSON.")
14
+ @click.pass_context
15
+ def me(ctx, json_mode: bool) -> None:
16
+ """Show current user info."""
17
+ json_mode = resolve_json_mode(json_mode)
18
+
19
+ with handle_errors(json_mode=json_mode):
20
+ with ChatGPTClient() as client:
21
+ data = client.get_me()
22
+
23
+ if json_mode:
24
+ print_json({"success": True, "data": data})
25
+ return
26
+
27
+ click.echo(f"Name: {data.get('name', 'Unknown')}")
28
+ click.echo(f"Email: {data.get('email', 'Unknown')}")
29
+ click.echo(f"ID: {data.get('id', 'Unknown')}")
30
+ orgs = data.get("orgs", {}).get("data", [])
31
+ if orgs:
32
+ org = orgs[0]
33
+ click.echo(f"Org: {org.get('title', 'Unknown')}")
34
+
35
+
36
+ @click.command("models")
37
+ @click.option("--json", "json_mode", is_flag=True, help="Output as JSON.")
38
+ @click.pass_context
39
+ def models(ctx, json_mode: bool) -> None:
40
+ """List available models."""
41
+ json_mode = resolve_json_mode(json_mode)
42
+
43
+ with handle_errors(json_mode=json_mode):
44
+ with ChatGPTClient() as client:
45
+ model_list = client.get_models()
46
+
47
+ if json_mode:
48
+ print_json({"success": True, "data": model_list})
49
+ return
50
+
51
+ click.echo(f"{'Slug':<30} {'Title':<30} {'Tags'}")
52
+ click.echo("-" * 80)
53
+ for m in model_list:
54
+ slug = m.get("slug", "?")
55
+ title = m.get("title", "?")
56
+ tags = ", ".join(m.get("tags", []))
57
+ click.echo(f"{slug:<30} {title:<30} {tags}")
@@ -0,0 +1,89 @@
1
+ """Auth management commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import click
6
+
7
+ from ..core.auth import clear_auth, is_logged_in, load_auth, login_browser
8
+ from ..utils.helpers import handle_errors
9
+ from ..utils.output import print_json
10
+
11
+
12
+ @click.group("auth")
13
+ def auth_group():
14
+ """Manage authentication."""
15
+
16
+
17
+ @auth_group.command("login")
18
+ @click.pass_context
19
+ def login(ctx) -> None:
20
+ """Login to ChatGPT via browser."""
21
+ json_mode = ctx.obj.get("json", False) if ctx.obj else False
22
+
23
+ with handle_errors(json_mode=json_mode):
24
+ auth_data = login_browser()
25
+ if json_mode:
26
+ print_json(
27
+ {
28
+ "success": True,
29
+ "data": {"message": "Logged in successfully"},
30
+ }
31
+ )
32
+ else:
33
+ click.echo("Logged in successfully.")
34
+ if auth_data.get("device_id"):
35
+ click.echo(f"Device ID: {auth_data['device_id']}")
36
+
37
+
38
+ @auth_group.command("status")
39
+ @click.pass_context
40
+ def status(ctx) -> None:
41
+ """Check authentication status."""
42
+ json_mode = ctx.obj.get("json", False) if ctx.obj else False
43
+
44
+ with handle_errors(json_mode=json_mode):
45
+ if not is_logged_in():
46
+ if json_mode:
47
+ print_json(
48
+ {
49
+ "success": True,
50
+ "data": {"logged_in": False},
51
+ }
52
+ )
53
+ else:
54
+ click.echo("Not logged in. Run: cli-web-chatgpt auth login")
55
+ return
56
+
57
+ auth = load_auth()
58
+ token_preview = auth["access_token"][:20] + "..."
59
+ device_id = auth.get("device_id", "unknown")
60
+
61
+ if json_mode:
62
+ print_json(
63
+ {
64
+ "success": True,
65
+ "data": {
66
+ "logged_in": True,
67
+ "device_id": device_id,
68
+ "token_preview": token_preview,
69
+ },
70
+ }
71
+ )
72
+ else:
73
+ click.echo("Logged in.")
74
+ click.echo(f"Device ID: {device_id}")
75
+ click.echo(f"Token: {token_preview}")
76
+
77
+
78
+ @auth_group.command("logout")
79
+ @click.pass_context
80
+ def logout(ctx) -> None:
81
+ """Remove stored credentials."""
82
+ json_mode = ctx.obj.get("json", False) if ctx.obj else False
83
+
84
+ with handle_errors(json_mode=json_mode):
85
+ clear_auth()
86
+ if json_mode:
87
+ print_json({"success": True, "data": {"message": "Logged out"}})
88
+ else:
89
+ click.echo("Logged out. Credentials removed.")
@@ -0,0 +1,146 @@
1
+ """Chat commands — ask questions and generate images with ChatGPT."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import click
6
+
7
+ from ..core.client import ChatGPTClient
8
+ from ..utils.helpers import handle_errors, resolve_json_mode
9
+ from ..utils.output import print_json
10
+
11
+ try:
12
+ from rich.console import Console
13
+ from rich.markdown import Markdown
14
+
15
+ _RICH = True
16
+ except ImportError:
17
+ _RICH = False
18
+
19
+
20
+ @click.group("chat")
21
+ def chat_group():
22
+ """Send messages and generate images with ChatGPT."""
23
+
24
+
25
+ @chat_group.command("ask")
26
+ @click.argument("question")
27
+ @click.option(
28
+ "--model",
29
+ default=None,
30
+ help="Model slug (e.g. gpt-5-4-thinking). Use 'models' command to list.",
31
+ )
32
+ @click.option("--conversation", default=None, help="Continue an existing conversation by ID.")
33
+ @click.option("--json", "json_mode", is_flag=True, help="Output as JSON.")
34
+ @click.pass_context
35
+ def ask(ctx, question: str, model: str | None, conversation: str | None, json_mode: bool) -> None:
36
+ """Ask ChatGPT a question."""
37
+ json_mode = resolve_json_mode(json_mode)
38
+
39
+ with handle_errors(json_mode=json_mode):
40
+ with ChatGPTClient() as client:
41
+ if _RICH and not json_mode:
42
+ console = Console(stderr=True)
43
+ with console.status("Thinking..."):
44
+ result = client.send_message(
45
+ message=question,
46
+ conversation_id=conversation,
47
+ model=model,
48
+ )
49
+ else:
50
+ result = client.send_message(
51
+ message=question,
52
+ conversation_id=conversation,
53
+ model=model,
54
+ )
55
+
56
+ text = result.get("text", "")
57
+ conv_id = result.get("conversation_id")
58
+
59
+ if json_mode:
60
+ print_json(
61
+ {
62
+ "success": True,
63
+ "data": {
64
+ "text": text,
65
+ "conversation_id": conv_id,
66
+ "model": model or "default",
67
+ },
68
+ }
69
+ )
70
+ else:
71
+ if _RICH and text:
72
+ Console().print(Markdown(text))
73
+ else:
74
+ click.echo(text or "(no response)")
75
+
76
+
77
+ @chat_group.command("image")
78
+ @click.argument("prompt")
79
+ @click.option("--style", default=None, help="Image style to prepend to prompt.")
80
+ @click.option("--output", "-o", default=None, type=click.Path(), help="Save image to file.")
81
+ @click.option("--conversation", default=None, help="Continue an existing conversation by ID.")
82
+ @click.option("--json", "json_mode", is_flag=True, help="Output as JSON.")
83
+ @click.pass_context
84
+ def image(
85
+ ctx,
86
+ prompt: str,
87
+ style: str | None,
88
+ output: str | None,
89
+ conversation: str | None,
90
+ json_mode: bool,
91
+ ) -> None:
92
+ """Generate an image with ChatGPT."""
93
+ json_mode = resolve_json_mode(json_mode)
94
+ full_prompt = f"{style} style: {prompt}" if style else prompt
95
+
96
+ with handle_errors(json_mode=json_mode):
97
+ with ChatGPTClient() as client:
98
+ if _RICH and not json_mode:
99
+ console = Console(stderr=True)
100
+ with console.status("Generating image..."):
101
+ result = client.send_message(
102
+ message=full_prompt,
103
+ conversation_id=conversation,
104
+ image_mode=True,
105
+ )
106
+ else:
107
+ result = client.send_message(
108
+ message=full_prompt,
109
+ conversation_id=conversation,
110
+ image_mode=True,
111
+ )
112
+
113
+ file_id = result.get("file_id")
114
+ conv_id = result.get("conversation_id")
115
+ download_url = result.get("download_url")
116
+ text = result.get("text", "")
117
+
118
+ # Auto-download if output path given
119
+ if output and download_url:
120
+ img_bytes = client.download_file(download_url)
121
+ with open(output, "wb") as f:
122
+ f.write(img_bytes)
123
+ if not json_mode:
124
+ click.echo(f"Image saved to {output} ({len(img_bytes)} bytes)")
125
+
126
+ if json_mode:
127
+ data = {
128
+ "file_id": file_id,
129
+ "conversation_id": conv_id,
130
+ "download_url": download_url,
131
+ "text": text or None,
132
+ }
133
+ if output and download_url:
134
+ data["saved_to"] = output
135
+ print_json({"success": True, "data": data})
136
+ elif not output:
137
+ if file_id:
138
+ click.echo(f"Image generated: file_id={file_id}")
139
+ if download_url:
140
+ click.echo(
141
+ f"Download: cli-web-chatgpt images download {file_id} -c {conv_id}"
142
+ )
143
+ elif text:
144
+ click.echo(text)
145
+ else:
146
+ click.echo("No image was generated.", err=True)
@@ -0,0 +1,87 @@
1
+ """Conversation management commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import click
6
+
7
+ from ..core.client import ChatGPTClient
8
+ from ..utils.helpers import handle_errors, resolve_json_mode
9
+ from ..utils.output import print_json, truncate
10
+
11
+
12
+ @click.group("conversations")
13
+ def conversations_group():
14
+ """List and view conversations."""
15
+
16
+
17
+ @conversations_group.command("list")
18
+ @click.option("--limit", "-n", default=20, help="Number of conversations to show.")
19
+ @click.option("--archived", is_flag=True, help="Show archived conversations.")
20
+ @click.option("--starred", is_flag=True, help="Show starred conversations only.")
21
+ @click.option("--json", "json_mode", is_flag=True, help="Output as JSON.")
22
+ @click.pass_context
23
+ def list_conversations(ctx, limit: int, archived: bool, starred: bool, json_mode: bool) -> None:
24
+ """List recent conversations."""
25
+ json_mode = resolve_json_mode(json_mode)
26
+
27
+ with handle_errors(json_mode=json_mode):
28
+ with ChatGPTClient() as client:
29
+ data = client.list_conversations(limit=limit, archived=archived, starred=starred)
30
+
31
+ if json_mode:
32
+ print_json({"success": True, "data": data})
33
+ return
34
+
35
+ items = data.get("items", [])
36
+ if not items:
37
+ click.echo("No conversations found.")
38
+ return
39
+
40
+ click.echo(f"{'Title':<50} {'Updated':<20} {'ID'}")
41
+ click.echo("-" * 100)
42
+ for conv in items:
43
+ title = truncate(conv.get("title", "Untitled"), 48)
44
+ updated = (conv.get("update_time") or "")[:19]
45
+ cid = conv.get("id", "")
46
+ click.echo(f"{title:<50} {updated:<20} {cid}")
47
+
48
+
49
+ @conversations_group.command("get")
50
+ @click.argument("conversation_id")
51
+ @click.option("--json", "json_mode", is_flag=True, help="Output as JSON.")
52
+ @click.pass_context
53
+ def get_conversation(ctx, conversation_id: str, json_mode: bool) -> None:
54
+ """View a conversation by ID."""
55
+ json_mode = resolve_json_mode(json_mode)
56
+
57
+ with handle_errors(json_mode=json_mode):
58
+ with ChatGPTClient() as client:
59
+ data = client.get_conversation(conversation_id)
60
+
61
+ if json_mode:
62
+ print_json({"success": True, "data": data})
63
+ return
64
+
65
+ title = data.get("title", "Untitled")
66
+ click.echo(f"Conversation: {title}")
67
+ click.echo(f"ID: {conversation_id}")
68
+ click.echo(f"Created: {data.get('create_time', 'unknown')}")
69
+ click.echo(f"Updated: {data.get('update_time', 'unknown')}")
70
+
71
+ mapping = data.get("mapping", {})
72
+ if mapping:
73
+ click.echo(f"\nMessages ({len(mapping)}):")
74
+ for _msg_id, node in mapping.items():
75
+ msg = node.get("message")
76
+ if not msg:
77
+ continue
78
+ role = msg.get("author", {}).get("role", "?")
79
+ content = msg.get("content", {})
80
+ parts = content.get("parts", [])
81
+ text = ""
82
+ for part in parts:
83
+ if isinstance(part, str):
84
+ text += part
85
+ if text:
86
+ preview = truncate(text, 80)
87
+ click.echo(f" [{role}] {preview}")