updated to run on Windows and add est capabilities
This commit is contained in:
@@ -58,8 +58,8 @@ def load_config() -> Config:
|
||||
claude_key = os.getenv("CLAUDE_SESSION_KEY", "").strip() or None
|
||||
export_dir = Path(os.getenv("EXPORT_DIR", "./exports")).expanduser()
|
||||
output_structure = os.getenv("OUTPUT_STRUCTURE", "provider/project/year").strip()
|
||||
cache_dir = Path(os.getenv("CACHE_DIR", "~/.ai-chat-exporter")).expanduser()
|
||||
log_file = os.getenv("LOG_FILE", "~/.ai-chat-exporter/logs/exporter.log").strip()
|
||||
cache_dir = Path(os.getenv("CACHE_DIR", "./cache")).expanduser()
|
||||
log_file = os.getenv("LOG_FILE", "./cache/logs/exporter.log").strip()
|
||||
|
||||
# Joplin
|
||||
joplin_token = os.getenv("JOPLIN_API_TOKEN", "").strip() or None
|
||||
@@ -101,7 +101,7 @@ def load_config() -> Config:
|
||||
if not chatgpt_token and not claude_key:
|
||||
logger.warning(
|
||||
"Neither CHATGPT_SESSION_TOKEN nor CLAUDE_SESSION_KEY is set. "
|
||||
"Run 'python -m src.main auth' to configure credentials."
|
||||
"Run 'ai-chat-exporter auth' to configure credentials."
|
||||
)
|
||||
|
||||
# Create and validate output directory
|
||||
@@ -173,7 +173,7 @@ def _validate_chatgpt_token(token: str) -> datetime | None:
|
||||
if delta.total_seconds() < 0:
|
||||
logger.warning(
|
||||
"CHATGPT_SESSION_TOKEN expired at %s. "
|
||||
"Run 'python -m src.main auth' to refresh it.",
|
||||
"Run 'ai-chat-exporter auth' to refresh it.",
|
||||
expiry.strftime("%Y-%m-%d %H:%M UTC"),
|
||||
)
|
||||
elif delta.total_seconds() < 86400:
|
||||
|
||||
49
src/main.py
49
src/main.py
@@ -70,7 +70,7 @@ def cli(ctx: click.Context, verbose: bool, quiet: bool, debug: bool, no_log_file
|
||||
|
||||
# Determine log file path from env (setup_logging handles "none")
|
||||
import os
|
||||
log_file = os.getenv("LOG_FILE", "~/.ai-chat-exporter/logs/exporter.log")
|
||||
log_file = os.getenv("LOG_FILE", "./cache/logs/exporter.log")
|
||||
|
||||
setup_logging(level=level, log_file=log_file, no_log_file=no_log_file)
|
||||
|
||||
@@ -79,7 +79,7 @@ def cli(ctx: click.Context, verbose: bool, quiet: bool, debug: bool, no_log_file
|
||||
|
||||
# Initialise cache (needed for ToS gate on every command)
|
||||
import os
|
||||
cache_dir = Path(os.getenv("CACHE_DIR", "~/.ai-chat-exporter")).expanduser()
|
||||
cache_dir = Path(os.getenv("CACHE_DIR", "./cache")).expanduser()
|
||||
try:
|
||||
cache = Cache(cache_dir)
|
||||
except CacheError as e:
|
||||
@@ -140,7 +140,7 @@ def auth(ctx: click.Context) -> None:
|
||||
if configure_claude:
|
||||
_auth_claude(os_name)
|
||||
|
||||
console.print("\n[green]Done! Run 'python -m src.main doctor' to verify your setup.[/green]")
|
||||
console.print("\n[green]Done! Run 'ai-chat-exporter doctor' to verify your setup.[/green]")
|
||||
|
||||
|
||||
def _auth_chatgpt(os_name: str) -> None:
|
||||
@@ -178,6 +178,25 @@ def _auth_chatgpt(os_name: str) -> None:
|
||||
except Exception:
|
||||
console.print("[yellow]Could not decode token expiry.[/yellow]")
|
||||
|
||||
# Live validation — exchange session token for an access token
|
||||
_valid = False
|
||||
_error: str | None = None
|
||||
with console.status("[dim]Validating token with ChatGPT API…[/dim]"):
|
||||
try:
|
||||
from src.providers.chatgpt import ChatGPTProvider
|
||||
_prov = ChatGPTProvider(session_token=token)
|
||||
_prov._fetch_access_token()
|
||||
_valid = True
|
||||
except ProviderError as e:
|
||||
_error = str(e.original)
|
||||
except Exception as e:
|
||||
_error = str(e)
|
||||
|
||||
if _valid:
|
||||
console.print("[green]✓ Token verified — connected to ChatGPT API.[/green]")
|
||||
else:
|
||||
console.print(f"[red]✗ Token validation failed: {_error}[/red]")
|
||||
|
||||
_write_token_to_env("CHATGPT_SESSION_TOKEN", token)
|
||||
|
||||
# --- ChatGPT Projects ---
|
||||
@@ -231,7 +250,25 @@ def _auth_claude(os_name: str) -> None:
|
||||
console.print("[yellow]Skipped Claude token.[/yellow]")
|
||||
return
|
||||
|
||||
console.print("[green]Claude session key saved.[/green]")
|
||||
# Live validation — fetch org ID (the first call any Claude operation makes)
|
||||
_valid = False
|
||||
_error: str | None = None
|
||||
with console.status("[dim]Validating token with Claude API…[/dim]"):
|
||||
try:
|
||||
from src.providers.claude import ClaudeProvider
|
||||
_prov = ClaudeProvider(session_key=key)
|
||||
_prov._get_org_id()
|
||||
_valid = True
|
||||
except ProviderError as e:
|
||||
_error = str(e.original)
|
||||
except Exception as e:
|
||||
_error = str(e)
|
||||
|
||||
if _valid:
|
||||
console.print("[green]✓ Token verified — connected to Claude API.[/green]")
|
||||
else:
|
||||
console.print(f"[red]✗ Token validation failed: {_error}[/red]")
|
||||
|
||||
_write_token_to_env("CLAUDE_SESSION_KEY", key)
|
||||
|
||||
|
||||
@@ -341,7 +378,7 @@ def _run_doctor_checks() -> list[dict]:
|
||||
|
||||
# Directories
|
||||
export_dir = Path(os.getenv("EXPORT_DIR", "./exports")).expanduser()
|
||||
cache_dir = Path(os.getenv("CACHE_DIR", "~/.ai-chat-exporter")).expanduser()
|
||||
cache_dir = Path(os.getenv("CACHE_DIR", "./cache")).expanduser()
|
||||
|
||||
for label, dirpath in [("Export dir writable", export_dir), ("Cache dir writable", cache_dir)]:
|
||||
try:
|
||||
@@ -496,7 +533,7 @@ def export(
|
||||
providers_to_run = _resolve_providers(provider, cfg)
|
||||
if not providers_to_run:
|
||||
err_console.print(
|
||||
"[red]No providers configured. Run 'python -m src.main auth' to set up tokens.[/red]"
|
||||
"[red]No providers configured. Run 'ai-chat-exporter auth' to set up tokens.[/red]"
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@@ -326,7 +326,7 @@ class BaseProvider(ABC):
|
||||
msg = (
|
||||
f"[{self.provider_name}] Authentication failed (401 Unauthorized). "
|
||||
"Your session token has likely expired. "
|
||||
"Run 'python -m src.main auth' to refresh your token."
|
||||
"Run 'ai-chat-exporter auth' to refresh your token."
|
||||
)
|
||||
logger.error(msg)
|
||||
raise ProviderError(
|
||||
|
||||
@@ -77,7 +77,7 @@ class ChatGPTProvider(BaseProvider):
|
||||
"init",
|
||||
RuntimeError(
|
||||
"CHATGPT_SESSION_TOKEN is not set. "
|
||||
"Run 'python -m src.main auth' to configure it."
|
||||
"Run 'ai-chat-exporter auth' to configure it."
|
||||
),
|
||||
)
|
||||
self._session_token = token
|
||||
@@ -157,7 +157,7 @@ class ChatGPTProvider(BaseProvider):
|
||||
"fetch_access_token",
|
||||
RuntimeError(
|
||||
"No accessToken in /api/auth/session response. "
|
||||
"Your session token may be expired — run 'python -m src.main auth' to refresh."
|
||||
"Your session token may be expired — run 'ai-chat-exporter auth' to refresh."
|
||||
),
|
||||
)
|
||||
return access_token
|
||||
@@ -169,7 +169,7 @@ class ChatGPTProvider(BaseProvider):
|
||||
"The session token is used to obtain a short-lived access token via /api/auth/session. "
|
||||
"To refresh: open chatgpt.com in Chrome → F12 → Application → Cookies "
|
||||
"→ find '__Secure-next-auth.session-token' → copy the value. "
|
||||
"Then run 'python -m src.main auth' or update CHATGPT_SESSION_TOKEN in .env."
|
||||
"Then run 'ai-chat-exporter auth' or update CHATGPT_SESSION_TOKEN in .env."
|
||||
)
|
||||
logger.error(msg)
|
||||
raise ProviderError(
|
||||
@@ -369,7 +369,7 @@ class ChatGPTProvider(BaseProvider):
|
||||
logger.info(
|
||||
"[chatgpt] No project IDs configured — skipping project conversations. "
|
||||
"To include projects, set CHATGPT_PROJECT_IDS in .env "
|
||||
"(see 'python -m src.main auth' for instructions)."
|
||||
"(see 'ai-chat-exporter auth' for instructions)."
|
||||
)
|
||||
return self._apply_since_filter(default_convs, since)
|
||||
|
||||
@@ -624,7 +624,10 @@ def _extract_messages(
|
||||
content_type = content_obj.get("content_type", "text")
|
||||
text = _extract_text(content_obj, conv_id, node_id)
|
||||
|
||||
if content_type != "text":
|
||||
# model_editable_context carries project instructions as plain text parts
|
||||
_TEXT_EXTRACTABLE = {"text", "model_editable_context"}
|
||||
|
||||
if content_type not in _TEXT_EXTRACTABLE:
|
||||
logger.warning(
|
||||
"[chatgpt] Skipping %s content in conversation %s message %s "
|
||||
"— rich content not yet supported (see FUTURE.md)",
|
||||
|
||||
@@ -39,7 +39,7 @@ class ClaudeProvider(BaseProvider):
|
||||
"init",
|
||||
RuntimeError(
|
||||
"CLAUDE_SESSION_KEY is not set. "
|
||||
"Run 'python -m src.main auth' to configure it."
|
||||
"Run 'ai-chat-exporter auth' to configure it."
|
||||
),
|
||||
)
|
||||
# Set sessionKey in the cookie jar
|
||||
@@ -60,7 +60,7 @@ class ClaudeProvider(BaseProvider):
|
||||
"Note: Claude session keys are opaque — a 401 is the only expiry signal. "
|
||||
"To refresh: open claude.ai in Chrome → F12 → Application → Cookies "
|
||||
"→ find 'sessionKey' → copy the value. "
|
||||
"Then run 'python -m src.main auth' or update CLAUDE_SESSION_KEY in .env."
|
||||
"Then run 'ai-chat-exporter auth' or update CLAUDE_SESSION_KEY in .env."
|
||||
)
|
||||
logger.error(msg)
|
||||
raise ProviderError(
|
||||
|
||||
Reference in New Issue
Block a user