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.
- cli_web/chatgpt/README.md +87 -0
- cli_web/chatgpt/__init__.py +3 -0
- cli_web/chatgpt/__main__.py +5 -0
- cli_web/chatgpt/chatgpt_cli.py +132 -0
- cli_web/chatgpt/commands/__init__.py +0 -0
- cli_web/chatgpt/commands/account.py +57 -0
- cli_web/chatgpt/commands/auth_cmd.py +89 -0
- cli_web/chatgpt/commands/chat.py +146 -0
- cli_web/chatgpt/commands/conversations.py +87 -0
- cli_web/chatgpt/commands/images.py +122 -0
- cli_web/chatgpt/core/__init__.py +0 -0
- cli_web/chatgpt/core/auth.py +227 -0
- cli_web/chatgpt/core/client.py +392 -0
- cli_web/chatgpt/core/exceptions.py +67 -0
- cli_web/chatgpt/skills/SKILL.md +103 -0
- cli_web/chatgpt/tests/TEST.md +126 -0
- cli_web/chatgpt/tests/__init__.py +0 -0
- cli_web/chatgpt/tests/test_core.py +349 -0
- cli_web/chatgpt/tests/test_e2e.py +288 -0
- cli_web/chatgpt/utils/__init__.py +0 -0
- cli_web/chatgpt/utils/doctor.py +188 -0
- cli_web/chatgpt/utils/helpers.py +74 -0
- cli_web/chatgpt/utils/mcp_server.py +290 -0
- cli_web/chatgpt/utils/output.py +17 -0
- cli_web/chatgpt/utils/repl_skin.py +486 -0
- cli_web_chatgpt-0.1.0.dist-info/METADATA +15 -0
- cli_web_chatgpt-0.1.0.dist-info/RECORD +30 -0
- cli_web_chatgpt-0.1.0.dist-info/WHEEL +5 -0
- cli_web_chatgpt-0.1.0.dist-info/entry_points.txt +2 -0
- cli_web_chatgpt-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -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,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}")
|