feat: v0.2.0 — Joplin import, ChatGPT Projects, --project filter

Core features:
- Add `joplin` command: syncs exported Markdown to Joplin via local REST API
- Notebooks auto-created per provider+project (e.g. "ChatGPT - My Project")
- Idempotent: notes updated (not duplicated) on re-run; note ID tracked in manifest
- Add `--project` filter to `export` and `list` commands (substring or 'none')
- Add ChatGPT Projects support via CHATGPT_PROJECT_IDS env var

Config:
- Add JOPLIN_API_TOKEN, JOPLIN_API_URL, JOPLIN_REQUEST_TIMEOUT
- Version now read from importlib.metadata (single source of truth: pyproject.toml)
- Bump version to 0.2.0

Quality:
- Explicit Timeout handling in JoplinClient with actionable error messages
- token validation (validate_token) separate from connectivity (ping)
- Remove debug_auth.py, debug_claude.py, and untracked .har file
- Add *.har to .gitignore (may contain auth cookies/session tokens)
- Update README, CHANGELOG, FUTURE.md to reflect v0.2.0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-01 06:04:03 -05:00
co-authored by Claude Sonnet 4.6
parent 23d7c17255
commit 304cf4fde4
16 changed files with 1795 additions and 133 deletions
+33
View File
@@ -35,6 +35,13 @@ class Config:
log_file: str
# Decoded ChatGPT JWT expiry (None if token absent or not a JWT)
chatgpt_token_expiry: datetime | None = field(default=None, repr=False)
# ChatGPT Project gizmo IDs (g-p-xxx) — project conversations are not
# included in the default /conversations listing; they must be fetched
# separately via /backend-api/gizmos/{id}/conversations.
chatgpt_project_ids: list[str] = field(default_factory=list)
# Joplin local REST API settings (Web Clipper service)
joplin_api_token: str | None = None
joplin_api_url: str = "http://localhost:41184"
def load_config() -> Config:
@@ -54,6 +61,24 @@ def load_config() -> Config:
cache_dir = Path(os.getenv("CACHE_DIR", "~/.ai-chat-exporter")).expanduser()
log_file = os.getenv("LOG_FILE", "~/.ai-chat-exporter/logs/exporter.log").strip()
# Joplin
joplin_token = os.getenv("JOPLIN_API_TOKEN", "").strip() or None
joplin_url = os.getenv("JOPLIN_API_URL", "http://localhost:41184").strip()
# Parse CHATGPT_PROJECT_IDS — comma-separated list of gizmo IDs (g-p-xxx)
_project_ids_raw = os.getenv("CHATGPT_PROJECT_IDS", "").strip()
chatgpt_project_ids = [
pid.strip()
for pid in _project_ids_raw.split(",")
if pid.strip() and pid.strip().startswith("g-p-")
] if _project_ids_raw else []
if _project_ids_raw and not chatgpt_project_ids:
logger.warning(
"CHATGPT_PROJECT_IDS is set but contains no valid project IDs. "
"Each ID should start with 'g-p-' (e.g. g-p-68c2b2b3037c8191890036fb4ae3ed9f). "
"Find your project ID in the browser URL when viewing a project."
)
errors: list[str] = []
# Validate output structure
@@ -108,6 +133,9 @@ def load_config() -> Config:
cache_dir=cache_dir,
log_file=log_file,
chatgpt_token_expiry=chatgpt_expiry,
chatgpt_project_ids=chatgpt_project_ids,
joplin_api_token=joplin_token,
joplin_api_url=joplin_url,
)
_log_startup_summary(config)
@@ -182,16 +210,21 @@ def _log_startup_summary(cfg: Config) -> None:
"""Log a single INFO line summarising the active configuration."""
chatgpt_status = format_token_status(cfg.chatgpt_session_token, cfg.chatgpt_token_expiry)
claude_status = format_token_status(cfg.claude_session_key)
joplin_status = "configured" if cfg.joplin_api_token else "not configured"
logger.info(
"Config loaded | "
"ChatGPT: %s | "
"Claude: %s | "
"chatgpt_projects: %d | "
"Joplin: %s | "
"export_dir=%s | "
"structure=%s | "
"cache_dir=%s",
chatgpt_status,
claude_status,
len(cfg.chatgpt_project_ids),
joplin_status,
cfg.export_dir,
cfg.output_structure,
cfg.cache_dir,