"
+ plan_raw = (access_claims.get("https://api.openai.com/auth") or {}).get("chatgpt_plan_type") or "unknown"
+ plan_map = {
+ "plus": "Plus",
+ "pro": "Pro",
+ "free": "Free",
+ "team": "Team",
+ "enterprise": "Enterprise",
+ }
+ plan = plan_map.get(str(plan_raw).lower(), str(plan_raw).title() if isinstance(plan_raw, str) else "Unknown")
+
+ print("👤 Account")
+ print(" • Signed in with ChatGPT")
+ print(f" • Login: {email}")
+ print(f" • Plan: {plan}")
+ if account_id:
+ print(f" • Account ID: {account_id}")
+ sys.exit(0)
+ else:
+ parser.error("Unknown command")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/chatmock/config.py b/chatmock/config.py
new file mode 100644
index 0000000..93e0914
--- /dev/null
+++ b/chatmock/config.py
@@ -0,0 +1,35 @@
+from __future__ import annotations
+
+import os
+import sys
+from pathlib import Path
+
+
+CLIENT_ID_DEFAULT = os.getenv("CHATGPT_LOCAL_CLIENT_ID") or "app_EMoamEEZ73f0CkXaXp7hrann"
+
+CHATGPT_RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses"
+
+
+def read_base_instructions() -> str:
+ candidates = [
+ Path(__file__).parent.parent / "prompt.md",
+ Path(__file__).parent / "prompt.md",
+ Path(getattr(sys, "_MEIPASS", "")) / "prompt.md" if getattr(sys, "_MEIPASS", None) else None,
+ Path.cwd() / "prompt.md",
+ ]
+ for p in candidates:
+ if not p:
+ continue
+ try:
+ if p.exists():
+ content = p.read_text(encoding="utf-8")
+ if isinstance(content, str) and content.strip():
+ return content
+ except Exception:
+ continue
+ raise FileNotFoundError(
+ "Failed to read prompt.md; expected adjacent to package or CWD."
+ )
+
+
+BASE_INSTRUCTIONS = read_base_instructions()
diff --git a/chatmock/http.py b/chatmock/http.py
new file mode 100644
index 0000000..567093a
--- /dev/null
+++ b/chatmock/http.py
@@ -0,0 +1,24 @@
+from __future__ import annotations
+
+from flask import Response, jsonify, request
+
+
+def build_cors_headers() -> dict:
+ origin = request.headers.get("Origin", "*")
+ req_headers = request.headers.get("Access-Control-Request-Headers")
+ allow_headers = req_headers if req_headers else "Authorization, Content-Type, Accept"
+ return {
+ "Access-Control-Allow-Origin": origin,
+ "Access-Control-Allow-Methods": "POST, GET, OPTIONS",
+ "Access-Control-Allow-Headers": allow_headers,
+ "Access-Control-Max-Age": "86400",
+ }
+
+
+def json_error(message: str, status: int = 400) -> Response:
+ resp = jsonify({"error": {"message": message}})
+ response: Response = Response(response=resp.response, status=status, mimetype="application/json")
+ for k, v in build_cors_headers().items():
+ response.headers.setdefault(k, v)
+ return response
+
diff --git a/models.py b/chatmock/models.py
similarity index 90%
rename from models.py
rename to chatmock/models.py
index 89dadd9..bb19ac4 100644
--- a/models.py
+++ b/chatmock/models.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
from dataclasses import dataclass
from typing import Optional
diff --git a/oauth.py b/chatmock/oauth.py
similarity index 98%
rename from oauth.py
rename to chatmock/oauth.py
index 395401e..7738cf8 100644
--- a/oauth.py
+++ b/chatmock/oauth.py
@@ -10,8 +10,8 @@ import urllib.parse
import urllib.request
from typing import Any, Dict, Tuple
-from models import AuthBundle, PkceCodes, TokenData
-from utils import eprint, generate_pkce, parse_jwt_claims, write_auth_file
+from .models import AuthBundle, PkceCodes, TokenData
+from .utils import eprint, generate_pkce, parse_jwt_claims, write_auth_file
REQUIRED_PORT = 1455
@@ -31,7 +31,7 @@ LOGIN_SUCCESS_HTML = """
You can now close this window and return to the terminal and run python3 chatmock.py serve to start the server.