updated to run on Windows and add est capabilities

This commit is contained in:
JesseMarkowitz
2026-03-30 11:08:05 -04:00
parent 304cf4fde4
commit 050cd49124
13 changed files with 524 additions and 54 deletions

View File

@@ -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:

View File

@@ -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)

View File

@@ -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(

View File

@@ -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)",

View File

@@ -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(