Full Code of lfnovo/open-notebook for AI

main a42e2a347ee2 cached
446 files
2.7 MB
733.7k tokens
1452 symbols
1 requests
Download .txt
Showing preview only (2,924K chars total). Download the full file or copy to clipboard to get everything.
Repository: lfnovo/open-notebook
Branch: main
Commit: a42e2a347ee2
Files: 446
Total size: 2.7 MB

Directory structure:
gitextract_5wx36h6b/

├── .dockerignore
├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.yml
│   │   ├── config.yml
│   │   ├── feature_request.yml
│   │   └── installation_issue.yml
│   ├── pull_request_template.md
│   └── workflows/
│       ├── build-and-release.yml
│       ├── build-dev.yml
│       ├── claude-code-review.yml
│       ├── claude.yml
│       └── test.yml
├── .gitignore
├── .python-version
├── .worktreeinclude
├── CHANGELOG.md
├── CLAUDE.md
├── CONFIGURATION.md
├── CONTRIBUTING.md
├── Dockerfile
├── Dockerfile.single
├── LICENSE
├── MAINTAINER_GUIDE.md
├── Makefile
├── README.dev.md
├── README.md
├── api/
│   ├── CLAUDE.md
│   ├── __init__.py
│   ├── auth.py
│   ├── chat_service.py
│   ├── client.py
│   ├── command_service.py
│   ├── context_service.py
│   ├── credentials_service.py
│   ├── embedding_service.py
│   ├── episode_profiles_service.py
│   ├── insights_service.py
│   ├── main.py
│   ├── models.py
│   ├── models_service.py
│   ├── notebook_service.py
│   ├── notes_service.py
│   ├── podcast_api_service.py
│   ├── podcast_service.py
│   ├── routers/
│   │   ├── __init__.py
│   │   ├── auth.py
│   │   ├── chat.py
│   │   ├── commands.py
│   │   ├── config.py
│   │   ├── context.py
│   │   ├── credentials.py
│   │   ├── embedding.py
│   │   ├── embedding_rebuild.py
│   │   ├── episode_profiles.py
│   │   ├── insights.py
│   │   ├── languages.py
│   │   ├── models.py
│   │   ├── notebooks.py
│   │   ├── notes.py
│   │   ├── podcasts.py
│   │   ├── search.py
│   │   ├── settings.py
│   │   ├── source_chat.py
│   │   ├── sources.py
│   │   ├── speaker_profiles.py
│   │   └── transformations.py
│   ├── search_service.py
│   ├── settings_service.py
│   ├── sources_service.py
│   └── transformations_service.py
├── commands/
│   ├── CLAUDE.md
│   ├── __init__.py
│   ├── embedding_commands.py
│   ├── example_commands.py
│   ├── podcast_commands.py
│   └── source_commands.py
├── docker-compose.yml
├── docs/
│   ├── 0-START-HERE/
│   │   ├── index.md
│   │   ├── quick-start-cloud.md
│   │   ├── quick-start-local.md
│   │   └── quick-start-openai.md
│   ├── 1-INSTALLATION/
│   │   ├── docker-compose.md
│   │   ├── from-source.md
│   │   ├── index.md
│   │   └── single-container.md
│   ├── 2-CORE-CONCEPTS/
│   │   ├── ai-context-rag.md
│   │   ├── chat-vs-transformations.md
│   │   ├── index.md
│   │   ├── notebooks-sources-notes.md
│   │   └── podcasts-explained.md
│   ├── 3-USER-GUIDE/
│   │   ├── adding-sources.md
│   │   ├── api-configuration.md
│   │   ├── chat-effectively.md
│   │   ├── citations.md
│   │   ├── creating-podcasts.md
│   │   ├── index.md
│   │   ├── interface-overview.md
│   │   ├── search.md
│   │   ├── transformations.md
│   │   └── working-with-notes.md
│   ├── 4-AI-PROVIDERS/
│   │   └── index.md
│   ├── 5-CONFIGURATION/
│   │   ├── advanced.md
│   │   ├── ai-providers.md
│   │   ├── database.md
│   │   ├── environment-reference.md
│   │   ├── index.md
│   │   ├── local-stt.md
│   │   ├── local-tts.md
│   │   ├── mcp-integration.md
│   │   ├── ollama.md
│   │   ├── openai-compatible.md
│   │   ├── reverse-proxy.md
│   │   └── security.md
│   ├── 6-TROUBLESHOOTING/
│   │   ├── ai-chat-issues.md
│   │   ├── connection-issues.md
│   │   ├── faq.md
│   │   ├── index.md
│   │   └── quick-fixes.md
│   ├── 7-DEVELOPMENT/
│   │   ├── api-reference.md
│   │   ├── architecture.md
│   │   ├── code-standards.md
│   │   ├── contributing.md
│   │   ├── design-principles.md
│   │   ├── development-setup.md
│   │   ├── index.md
│   │   ├── maintainer-guide.md
│   │   ├── quick-start.md
│   │   └── testing.md
│   ├── SECURITY_REVIEW.md
│   └── index.md
├── examples/
│   ├── README.md
│   ├── docker-compose-dev.yml
│   ├── docker-compose-full-local.yml
│   ├── docker-compose-ollama.yml
│   ├── docker-compose-single.yml
│   └── docker-compose-speaches.yml
├── frontend/
│   ├── .gitignore
│   ├── components.json
│   ├── eslint.config.mjs
│   ├── next.config.ts
│   ├── package.json
│   ├── postcss.config.mjs
│   ├── src/
│   │   ├── CLAUDE.md
│   │   ├── app/
│   │   │   ├── (auth)/
│   │   │   │   └── login/
│   │   │   │       └── page.tsx
│   │   │   ├── (dashboard)/
│   │   │   │   ├── advanced/
│   │   │   │   │   ├── components/
│   │   │   │   │   │   ├── RebuildEmbeddings.tsx
│   │   │   │   │   │   └── SystemInfo.tsx
│   │   │   │   │   └── page.tsx
│   │   │   │   ├── layout.tsx
│   │   │   │   ├── notebooks/
│   │   │   │   │   ├── [id]/
│   │   │   │   │   │   └── page.tsx
│   │   │   │   │   ├── components/
│   │   │   │   │   │   ├── ChatColumn.test.tsx
│   │   │   │   │   │   ├── ChatColumn.tsx
│   │   │   │   │   │   ├── NoteEditorDialog.tsx
│   │   │   │   │   │   ├── NotebookCard.tsx
│   │   │   │   │   │   ├── NotebookDeleteDialog.tsx
│   │   │   │   │   │   ├── NotebookHeader.tsx
│   │   │   │   │   │   ├── NotebookList.tsx
│   │   │   │   │   │   ├── NotesColumn.tsx
│   │   │   │   │   │   └── SourcesColumn.tsx
│   │   │   │   │   └── page.tsx
│   │   │   │   ├── page.tsx
│   │   │   │   ├── podcasts/
│   │   │   │   │   └── page.tsx
│   │   │   │   ├── search/
│   │   │   │   │   └── page.tsx
│   │   │   │   ├── settings/
│   │   │   │   │   ├── api-keys/
│   │   │   │   │   │   └── page.tsx
│   │   │   │   │   ├── components/
│   │   │   │   │   │   └── SettingsForm.tsx
│   │   │   │   │   └── page.tsx
│   │   │   │   ├── sources/
│   │   │   │   │   ├── [id]/
│   │   │   │   │   │   └── page.tsx
│   │   │   │   │   └── page.tsx
│   │   │   │   └── transformations/
│   │   │   │       ├── components/
│   │   │   │       │   ├── DefaultPromptEditor.tsx
│   │   │   │       │   ├── TransformationCard.tsx
│   │   │   │       │   ├── TransformationEditorDialog.tsx
│   │   │   │       │   ├── TransformationPlayground.tsx
│   │   │   │       │   └── TransformationsList.tsx
│   │   │   │       └── page.tsx
│   │   │   ├── config/
│   │   │   │   └── route.ts
│   │   │   ├── globals.css
│   │   │   ├── layout.tsx
│   │   │   └── page.tsx
│   │   ├── components/
│   │   │   ├── auth/
│   │   │   │   └── LoginForm.tsx
│   │   │   ├── common/
│   │   │   │   ├── CommandPalette.tsx
│   │   │   │   ├── ConfirmDialog.test.tsx
│   │   │   │   ├── ConfirmDialog.tsx
│   │   │   │   ├── ConnectionGuard.tsx
│   │   │   │   ├── ContextIndicator.tsx
│   │   │   │   ├── ContextToggle.tsx
│   │   │   │   ├── EmptyState.tsx
│   │   │   │   ├── ErrorBoundary.tsx
│   │   │   │   ├── InlineEdit.tsx
│   │   │   │   ├── LanguageLoadingOverlay.tsx
│   │   │   │   ├── LanguageToggle.tsx
│   │   │   │   ├── LoadingSpinner.tsx
│   │   │   │   ├── ModelSelector.tsx
│   │   │   │   └── ThemeToggle.tsx
│   │   │   ├── errors/
│   │   │   │   └── ConnectionErrorOverlay.tsx
│   │   │   ├── layout/
│   │   │   │   ├── AppShell.tsx
│   │   │   │   ├── AppSidebar.test.tsx
│   │   │   │   ├── AppSidebar.tsx
│   │   │   │   └── SetupBanner.tsx
│   │   │   ├── notebooks/
│   │   │   │   ├── CollapsibleColumn.tsx
│   │   │   │   └── CreateNotebookDialog.tsx
│   │   │   ├── podcasts/
│   │   │   │   ├── EpisodeCard.tsx
│   │   │   │   ├── EpisodeProfilesPanel.tsx
│   │   │   │   ├── EpisodesTab.tsx
│   │   │   │   ├── GeneratePodcastDialog.tsx
│   │   │   │   ├── SpeakerProfilesPanel.tsx
│   │   │   │   ├── TemplatesTab.tsx
│   │   │   │   └── forms/
│   │   │   │       ├── EpisodeProfileFormDialog.tsx
│   │   │   │       └── SpeakerProfileFormDialog.tsx
│   │   │   ├── providers/
│   │   │   │   ├── I18nProvider.tsx
│   │   │   │   ├── ModalProvider.tsx
│   │   │   │   ├── QueryProvider.tsx
│   │   │   │   └── ThemeProvider.tsx
│   │   │   ├── search/
│   │   │   │   ├── AdvancedModelsDialog.tsx
│   │   │   │   ├── SaveToNotebooksDialog.tsx
│   │   │   │   └── StreamingResponse.tsx
│   │   │   ├── settings/
│   │   │   │   ├── EmbeddingModelChangeDialog.tsx
│   │   │   │   ├── MigrationBanner.tsx
│   │   │   │   ├── ModelTestResultDialog.tsx
│   │   │   │   └── index.ts
│   │   │   ├── source/
│   │   │   │   ├── ChatPanel.tsx
│   │   │   │   ├── MessageActions.tsx
│   │   │   │   ├── ModelSelector.tsx
│   │   │   │   ├── NotebookAssociations.tsx
│   │   │   │   ├── SessionManager.tsx
│   │   │   │   ├── SourceDetailContent.tsx
│   │   │   │   ├── SourceDialog.tsx
│   │   │   │   └── SourceInsightDialog.tsx
│   │   │   ├── sources/
│   │   │   │   ├── AddExistingSourceDialog.tsx
│   │   │   │   ├── AddSourceButton.tsx
│   │   │   │   ├── AddSourceDialog.tsx
│   │   │   │   ├── README.md
│   │   │   │   ├── SourceCard.tsx
│   │   │   │   ├── index.ts
│   │   │   │   └── steps/
│   │   │   │       ├── NotebooksStep.tsx
│   │   │   │       ├── ProcessingStep.tsx
│   │   │   │       └── SourceTypeStep.tsx
│   │   │   └── ui/
│   │   │       ├── CLAUDE.md
│   │   │       ├── accordion.tsx
│   │   │       ├── alert-dialog.tsx
│   │   │       ├── alert.tsx
│   │   │       ├── badge.tsx
│   │   │       ├── button.tsx
│   │   │       ├── card.tsx
│   │   │       ├── checkbox-list.tsx
│   │   │       ├── checkbox.tsx
│   │   │       ├── collapsible.tsx
│   │   │       ├── command.tsx
│   │   │       ├── dialog.tsx
│   │   │       ├── dropdown-menu.tsx
│   │   │       ├── form-section.tsx
│   │   │       ├── input.tsx
│   │   │       ├── label.tsx
│   │   │       ├── markdown-editor.tsx
│   │   │       ├── popover.tsx
│   │   │       ├── progress.tsx
│   │   │       ├── radio-group.tsx
│   │   │       ├── scroll-area.tsx
│   │   │       ├── select.tsx
│   │   │       ├── separator.tsx
│   │   │       ├── sonner.tsx
│   │   │       ├── tabs.tsx
│   │   │       ├── textarea.tsx
│   │   │       ├── tooltip.tsx
│   │   │       └── wizard-container.tsx
│   │   ├── lib/
│   │   │   ├── api/
│   │   │   │   ├── CLAUDE.md
│   │   │   │   ├── chat.ts
│   │   │   │   ├── client.ts
│   │   │   │   ├── credentials.ts
│   │   │   │   ├── embedding.ts
│   │   │   │   ├── insights.ts
│   │   │   │   ├── models.ts
│   │   │   │   ├── notebooks.ts
│   │   │   │   ├── notes.ts
│   │   │   │   ├── podcasts.ts
│   │   │   │   ├── query-client.ts
│   │   │   │   ├── search.ts
│   │   │   │   ├── settings.ts
│   │   │   │   ├── source-chat.ts
│   │   │   │   ├── sources.ts
│   │   │   │   └── transformations.ts
│   │   │   ├── config.test.ts
│   │   │   ├── config.ts
│   │   │   ├── hooks/
│   │   │   │   ├── CLAUDE.md
│   │   │   │   ├── use-ask.ts
│   │   │   │   ├── use-auth.ts
│   │   │   │   ├── use-create-dialogs.tsx
│   │   │   │   ├── use-credentials.ts
│   │   │   │   ├── use-insights.ts
│   │   │   │   ├── use-media-query.ts
│   │   │   │   ├── use-modal-manager.test.ts
│   │   │   │   ├── use-modal-manager.ts
│   │   │   │   ├── use-models.ts
│   │   │   │   ├── use-navigation.ts
│   │   │   │   ├── use-notebooks.ts
│   │   │   │   ├── use-notes.ts
│   │   │   │   ├── use-podcasts.ts
│   │   │   │   ├── use-search.ts
│   │   │   │   ├── use-settings.ts
│   │   │   │   ├── use-sources.ts
│   │   │   │   ├── use-toast.ts
│   │   │   │   ├── use-transformations.ts
│   │   │   │   ├── use-translation.test.ts
│   │   │   │   ├── use-translation.ts
│   │   │   │   ├── use-version-check.ts
│   │   │   │   ├── useNotebookChat.ts
│   │   │   │   └── useSourceChat.ts
│   │   │   ├── i18n-events.ts
│   │   │   ├── i18n.ts
│   │   │   ├── locales/
│   │   │   │   ├── CLAUDE.md
│   │   │   │   ├── bn-IN/
│   │   │   │   │   └── index.ts
│   │   │   │   ├── en-US/
│   │   │   │   │   └── index.ts
│   │   │   │   ├── fr-FR/
│   │   │   │   │   └── index.ts
│   │   │   │   ├── index.test.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── it-IT/
│   │   │   │   │   └── index.ts
│   │   │   │   ├── ja-JP/
│   │   │   │   │   └── index.ts
│   │   │   │   ├── pt-BR/
│   │   │   │   │   └── index.ts
│   │   │   │   ├── ru-RU/
│   │   │   │   │   └── index.ts
│   │   │   │   ├── zh-CN/
│   │   │   │   │   └── index.ts
│   │   │   │   └── zh-TW/
│   │   │   │       └── index.ts
│   │   │   ├── stores/
│   │   │   │   ├── CLAUDE.md
│   │   │   │   ├── auth-store.ts
│   │   │   │   ├── navigation-store.ts
│   │   │   │   ├── notebook-columns-store.ts
│   │   │   │   ├── sidebar-store.ts
│   │   │   │   └── theme-store.ts
│   │   │   ├── theme-script.ts
│   │   │   ├── types/
│   │   │   │   ├── api.ts
│   │   │   │   ├── auth.ts
│   │   │   │   ├── common.ts
│   │   │   │   ├── config.ts
│   │   │   │   ├── models.ts
│   │   │   │   ├── podcasts.ts
│   │   │   │   ├── search.ts
│   │   │   │   └── transformations.ts
│   │   │   ├── utils/
│   │   │   │   ├── date-locale.ts
│   │   │   │   ├── error-handler.ts
│   │   │   │   └── source-references.tsx
│   │   │   └── utils.ts
│   │   ├── proxy.ts
│   │   └── test/
│   │       ├── jest-dom.d.ts
│   │       └── setup.ts
│   ├── start-server.js
│   ├── tailwind.config.ts
│   ├── tsconfig.json
│   └── vitest.config.ts
├── mypy.ini
├── open_notebook/
│   ├── CLAUDE.md
│   ├── __init__.py
│   ├── ai/
│   │   ├── CLAUDE.md
│   │   ├── __init__.py
│   │   ├── connection_tester.py
│   │   ├── key_provider.py
│   │   ├── model_discovery.py
│   │   ├── models.py
│   │   └── provision.py
│   ├── config.py
│   ├── database/
│   │   ├── CLAUDE.md
│   │   ├── async_migrate.py
│   │   ├── migrate.py
│   │   ├── migrations/
│   │   │   ├── 1.surrealql
│   │   │   ├── 10.surrealql
│   │   │   ├── 10_down.surrealql
│   │   │   ├── 11.surrealql
│   │   │   ├── 11_down.surrealql
│   │   │   ├── 12.surrealql
│   │   │   ├── 12_down.surrealql
│   │   │   ├── 13.surrealql
│   │   │   ├── 13_down.surrealql
│   │   │   ├── 14.surrealql
│   │   │   ├── 14_down.surrealql
│   │   │   ├── 1_down.surrealql
│   │   │   ├── 2.surrealql
│   │   │   ├── 2_down.surrealql
│   │   │   ├── 3.surrealql
│   │   │   ├── 3_down.surrealql
│   │   │   ├── 4.surrealql
│   │   │   ├── 4_down.surrealql
│   │   │   ├── 5.surrealql
│   │   │   ├── 5_down.surrealql
│   │   │   ├── 6.surrealql
│   │   │   ├── 6_down.surrealql
│   │   │   ├── 7.surrealql
│   │   │   ├── 7_down.surrealql
│   │   │   ├── 8.surrealql
│   │   │   ├── 8_down.surrealql
│   │   │   ├── 9.surrealql
│   │   │   └── 9_down.surrealql
│   │   └── repository.py
│   ├── domain/
│   │   ├── CLAUDE.md
│   │   ├── __init__.py
│   │   ├── base.py
│   │   ├── content_settings.py
│   │   ├── credential.py
│   │   ├── notebook.py
│   │   ├── provider_config.py
│   │   └── transformation.py
│   ├── exceptions.py
│   ├── graphs/
│   │   ├── CLAUDE.md
│   │   ├── ask.py
│   │   ├── chat.py
│   │   ├── prompt.py
│   │   ├── source.py
│   │   ├── source_chat.py
│   │   ├── tools.py
│   │   └── transformation.py
│   ├── podcasts/
│   │   ├── CLAUDE.md
│   │   ├── __init__.py
│   │   ├── migration.py
│   │   └── models.py
│   └── utils/
│       ├── CLAUDE.md
│       ├── README.md
│       ├── __init__.py
│       ├── chunking.py
│       ├── context_builder.py
│       ├── embedding.py
│       ├── encryption.py
│       ├── error_classifier.py
│       ├── graph_utils.py
│       ├── text_utils.py
│       ├── token_utils.py
│       └── version_utils.py
├── prompts/
│   ├── CLAUDE.md
│   ├── ask/
│   │   ├── entry.jinja
│   │   ├── final_answer.jinja
│   │   └── query_process.jinja
│   ├── chat/
│   │   └── system.jinja
│   ├── podcast/
│   │   ├── outline.jinja
│   │   └── transcript.jinja
│   └── source_chat/
│       └── system.jinja
├── pyproject.toml
├── run_api.py
├── scripts/
│   ├── README.md
│   ├── export_docs.py
│   └── wait-for-api.sh
├── supervisord.conf
├── supervisord.single.conf
└── tests/
    ├── README.md
    ├── conftest.py
    ├── test_chunking.py
    ├── test_domain.py
    ├── test_embedding.py
    ├── test_graphs.py
    ├── test_models_api.py
    ├── test_notes_api.py
    ├── test_podcast_path.py
    ├── test_url_validation.py
    └── test_utils.py

================================================
FILE CONTENTS
================================================

================================================
FILE: .dockerignore
================================================
# Git
.git
.gitignore

# Python
__pycache__
*.pyc
*.pyo
*.pyd
.venv
venv
ENV
env
.pytest_cache
.mypy_cache
.ruff_cache

# Frontend
frontend/node_modules
frontend/.next
frontend/dist
frontend/out
frontend/.env*
frontend/*.log

# Project data
.antigravity
.gemini
tmp
data
mydata
notebook_data
surreal_data
surreal-data
surreal_single_data
*.db
*.log
docker.env
.env
docker-compose*

# Documentation & CI (not needed in image)
docs
.github

# IDE and OS files
.vscode
.idea
*.swp
*.swo
*~
.DS_Store

================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.yml
================================================
name: 🐛 Bug Report
description: Report a bug or unexpected behavior (app is running but misbehaving)
title: "[Bug]: "
labels: ["bug", "needs-triage"]
body:
  - type: markdown
    attributes:
      value: |
        Thanks for reporting a bug! Please fill out the information below to help us understand and fix the issue.

        **Note**: If you're having installation or setup issues, please use the "Installation Issue" template instead.

  - type: textarea
    id: what-happened
    attributes:
      label: What did you do when it broke?
      description: Describe the steps you took that led to the bug
      placeholder: |
        1. I went to the Notebooks page
        2. I clicked on "Create New Notebook"
        3. I filled in the form and clicked "Save"
        4. Then the error occurred...
    validations:
      required: true

  - type: textarea
    id: how-broke
    attributes:
      label: How did it break?
      description: What happened that was unexpected? What did you expect to happen instead?
      placeholder: |
        Expected: The notebook should be created and I should see it in the list
        Actual: I got an error message saying "Failed to create notebook"
    validations:
      required: true

  - type: textarea
    id: logs-screenshots
    attributes:
      label: Logs or Screenshots
      description: |
        Please provide any error messages, logs, or screenshots that might help us understand the issue.

        **How to get logs:**
        - Docker: `docker compose logs -f open_notebook`
        - Check browser console (F12 → Console tab)
      placeholder: |
        Paste logs here or drag and drop screenshots.

        Error messages, stack traces, or browser console errors are very helpful!
    validations:
      required: false

  - type: dropdown
    id: version
    attributes:
      label: Open Notebook Version
      description: Which version are you using?
      options:
        - v1-latest (Docker)
        - v1-latest-single (Docker)
        - Latest from main branch
        - Other (please specify in additional context)
    validations:
      required: true

  - type: textarea
    id: environment
    attributes:
      label: Environment
      description: What environment are you running in?
      placeholder: |
        - OS: Ubuntu 22.04 / Windows 11 / macOS 14
        - Browser: Chrome 120
    validations:
      required: false

  - type: textarea
    id: additional-context
    attributes:
      label: Additional Context
      description: Any other information that might be helpful
      placeholder: "This started happening after I upgraded to v1.5.0..."
    validations:
      required: false

  - type: checkboxes
    id: willing-to-contribute
    attributes:
      label: Contribution
      description: Would you like to work on fixing this bug?
      options:
        - label: I am a developer and would like to work on fixing this issue (pending maintainer approval)
          required: false

  - type: markdown
    attributes:
      value: |
        ---
        **Next Steps:**
        1. A maintainer will review your bug report
        2. If you checked the box above and want to fix it, please propose your solution approach
        3. Wait for assignment before starting development
        4. See our [Contributing Guide](https://github.com/lfnovo/open-notebook/blob/main/CONTRIBUTING.md) for more details


================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: false
contact_links:
  - name: 💬 Discord Community
    url: https://discord.gg/37XJPXfz2w
    about: Get help from the community and share ideas
  - name: 🤖 Installation Assistant (ChatGPT)
    url: https://chatgpt.com/g/g-68776e2765b48191bd1bae3f30212631-open-notebook-installation-assistant
    about: CustomGPT that knows all our docs. Really useful. Try it.
  - name: 📚 Documentation
    url: https://github.com/lfnovo/open-notebook/tree/main/docs
    about: Browse our comprehensive documentation


================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.yml
================================================
name: ✨ Feature Suggestion
description: Suggest a new feature or improvement for Open Notebook
title: "[Feature]: "
labels: ["enhancement", "needs-triage"]
body:
  - type: markdown
    attributes:
      value: |
        Thanks for taking the time to suggest a feature! Your ideas help make Open Notebook better for everyone.

  - type: textarea
    id: feature-description
    attributes:
      label: Feature Description
      description: What feature would you like to see added or improved?
      placeholder: "I would like to be able to..."
    validations:
      required: true

  - type: textarea
    id: why-helpful
    attributes:
      label: Why would this be helpful?
      description: Explain how this feature would benefit you and other users
      placeholder: "This would help because..."
    validations:
      required: true

  - type: textarea
    id: proposed-solution
    attributes:
      label: Proposed Solution (Optional)
      description: If you have ideas on how to implement this feature, please share them
      placeholder: "This could be implemented by..."
    validations:
      required: false

  - type: textarea
    id: additional-context
    attributes:
      label: Additional Context
      description: Any other context, screenshots, or examples that might be helpful
      placeholder: "For example, other tools do this by..."
    validations:
      required: false

  - type: checkboxes
    id: willing-to-contribute
    attributes:
      label: Contribution
      description: Would you like to work on implementing this feature?
      options:
        - label: I am a developer and would like to work on implementing this feature (pending maintainer approval)
          required: false

  - type: markdown
    attributes:
      value: |
        ---
        **Next Steps:**
        1. A maintainer will review your feature request
        2. If approved and you checked the box above, the issue will be assigned to you
        3. Please wait for assignment before starting development
        4. See our [Contributing Guide](https://github.com/lfnovo/open-notebook/blob/main/CONTRIBUTING.md) for more details



================================================
FILE: .github/ISSUE_TEMPLATE/installation_issue.yml
================================================
name: 🔧 Installation Issue
description: Report problems with installation, setup, or connectivity
title: "[Install]: "
labels: ["installation", "needs-triage"]
body:
  - type: markdown
    attributes:
      value: |
        ## ⚠️ Before You Continue

        **Please try these resources first:**

        1. 🤖 **[Installation Assistant ChatGPT](https://chatgpt.com/g/g-68776e2765b48191bd1bae3f30212631-open-notebook-installation-assistant)** - Our AI assistant can help you troubleshoot most installation issues instantly!

        2. 📚 **[Installation Guide](https://github.com/lfnovo/open-notebook/blob/main/docs/getting-started/installation.md)** - Comprehensive setup instructions

        3. 🐋 **[Docker Deployment Guide](https://github.com/lfnovo/open-notebook/blob/main/docs/deployment/docker.md)** - Detailed Docker setup

        4. 🦙 **Ollama Issues?** Read our [Ollama Guide](https://github.com/lfnovo/open-notebook/blob/main/docs/features/ollama.md) first

        5. 💬 **[Discord Community](https://discord.gg/37XJPXfz2w)** - Get real-time help from the community

        ---

        If you've tried the above and still need help, please fill out the form below with as much detail as possible.

  - type: dropdown
    id: installation-method
    attributes:
      label: Installation Method
      description: How are you trying to install Open Notebook?
      options:
        - Docker (single container - v1-latest-single)
        - Docker (multi-container - docker-compose)
        - Local development (make start-all)
        - Other (please specify below)
    validations:
      required: true

  - type: textarea
    id: issue-description
    attributes:
      label: What is the issue?
      description: Describe the installation or setup problem you're experiencing
      placeholder: |
        Example: "I can't connect to the database" or "The container won't start" or "Getting 404 errors when accessing the UI"
    validations:
      required: true

  - type: textarea
    id: logs
    attributes:
      label: Logs
      description: |
        Please provide relevant logs. **This is very important for diagnosing issues!**

        **How to get logs:**
        - Docker single container: `docker logs open-notebook`
        - Docker Compose: `docker compose logs -f`
        - Specific service: `docker compose logs -f open_notebook`
      placeholder: |
        Paste your logs here. Include the full error message and stack trace if available.
      render: shell
    validations:
      required: false

  - type: textarea
    id: docker-compose
    attributes:
      label: Docker Compose Configuration
      description: |
        If using Docker Compose, please paste your `docker-compose.yml` file here.

        **⚠️ IMPORTANT: Redact any sensitive information (API keys, passwords, etc.)**
      placeholder: |
        services:
          open_notebook:
            image: lfnovo/open_notebook:v1-latest-single
            ports:
              - "8502:8502"
              - "5055:5055"
            environment:
              - OPENAI_API_KEY=sk-***REDACTED***
            ...
      render: yaml
    validations:
      required: false

  - type: textarea
    id: env-file
    attributes:
      label: Environment File
      description: |
        If using an `.env` or `docker.env` file, please paste it here.

        **⚠️ IMPORTANT: REDACT ALL API KEYS AND PASSWORDS!**
      placeholder: |
        SURREAL_URL=ws://surrealdb:8000/rpc
        SURREAL_USER=root
        SURREAL_PASSWORD=***REDACTED***
        OPENAI_API_KEY=sk-***REDACTED***
        ANTHROPIC_API_KEY=sk-ant-***REDACTED***
      render: shell
    validations:
      required: false

  - type: textarea
    id: system-info
    attributes:
      label: System Information
      description: Tell us about your setup
      placeholder: |
        - Operating System: Ubuntu 22.04 / Windows 11 / macOS 14
        - Docker version: `docker --version`
        - Docker Compose version: `docker compose version`
        - Architecture: amd64 / arm64 (Apple Silicon)
        - Available disk space: `df -h`
        - Available memory: `free -h` (Linux) or Activity Monitor (Mac)
    validations:
      required: false

  - type: textarea
    id: additional-context
    attributes:
      label: Additional Context
      description: Any other information that might be helpful
      placeholder: |
        - Are you behind a corporate proxy or firewall?
        - Are you using a VPN?
        - Have you made any custom modifications?
        - Did this work before and suddenly break?
    validations:
      required: false

  - type: checkboxes
    id: checklist
    attributes:
      label: Pre-submission Checklist
      description: Please confirm you've tried these steps
      options:
        - label: I tried the [Installation Assistant ChatGPT](https://chatgpt.com/g/g-68776e2765b48191bd1bae3f30212631-open-notebook-installation-assistant)
          required: false
        - label: I read the relevant documentation ([Installation Guide](https://github.com/lfnovo/open-notebook/blob/main/docs/getting-started/installation.md) or [Ollama Guide](https://github.com/lfnovo/open-notebook/blob/main/docs/features/ollama.md))
          required: false
        - label: I searched existing issues to see if this was already reported
          required: true
        - label: I redacted all sensitive information (API keys, passwords, etc.)
          required: true


================================================
FILE: .github/pull_request_template.md
================================================
## Description

<!-- Provide a clear and concise description of what this PR does -->

## Related Issue

<!-- This PR should be linked to an approved issue. If not, please create an issue first. -->

Fixes #<!-- issue number -->

## Type of Change

<!-- Mark the relevant option with an "x" -->

- [ ] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] Documentation update
- [ ] Code refactoring (no functional changes)
- [ ] Performance improvement
- [ ] Test coverage improvement

## How Has This Been Tested?

<!-- Describe the tests you ran and/or how you verified your changes work -->

- [ ] Tested locally with Docker
- [ ] Tested locally with development setup
- [ ] Added new unit tests
- [ ] Existing tests pass (`uv run pytest`)
- [ ] Manual testing performed (describe below)

**Test Details:**
<!-- Describe your testing approach -->

## Design Alignment

<!-- This section helps ensure your PR aligns with our project vision -->

**Which design principles does this PR support?** (See [DESIGN_PRINCIPLES.md](../DESIGN_PRINCIPLES.md))

- [ ] Privacy First
- [ ] Simplicity Over Features
- [ ] API-First Architecture
- [ ] Multi-Provider Flexibility
- [ ] Extensibility Through Standards
- [ ] Async-First for Performance

**Explanation:**
<!-- Brief explanation of how your changes align with these principles -->

## Checklist

<!-- Mark completed items with an "x" -->

### Code Quality
- [ ] My code follows PEP 8 style guidelines (Python)
- [ ] My code follows TypeScript best practices (Frontend)
- [ ] I have added type hints to my code (Python)
- [ ] I have added JSDoc comments where appropriate (TypeScript)
- [ ] I have performed a self-review of my code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] My changes generate no new warnings or errors

### Testing
- [ ] I have added tests that prove my fix is effective or that my feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] I ran linting: `make ruff` or `ruff check . --fix`
- [ ] I ran type checking: `make lint` or `uv run python -m mypy .`

### Documentation
- [ ] I have updated the relevant documentation in `/docs` (if applicable)
- [ ] I have added/updated docstrings for new/modified functions
- [ ] I have updated the API documentation (if API changes were made)
- [ ] I have added comments to complex logic

### Database Changes
- [ ] I have created migration scripts for any database schema changes (in `/migrations`)
- [ ] Migration includes both up and down scripts
- [ ] Migration has been tested locally

### Breaking Changes
- [ ] This PR includes breaking changes
- [ ] I have documented the migration path for users
- [ ] I have updated MIGRATION.md (if applicable)

## Screenshots (if applicable)

<!-- Add screenshots for UI changes -->

## Additional Context

<!-- Add any other context about the PR here -->

## Pre-Submission Verification

Before submitting, please verify:

- [ ] I have read [CONTRIBUTING.md](../CONTRIBUTING.md)
- [ ] I have read [DESIGN_PRINCIPLES.md](../DESIGN_PRINCIPLES.md)
- [ ] This PR addresses an approved issue that was assigned to me
- [ ] I have not included unrelated changes in this PR
- [ ] My PR title follows conventional commits format (e.g., "feat: add user authentication")

---

**Thank you for contributing to Open Notebook!** 🎉


================================================
FILE: .github/workflows/build-and-release.yml
================================================
name: Build and Release

on:
  workflow_dispatch:
    inputs:
      push_latest:
        description: 'Also push v1-latest tags'
        required: true
        default: false
        type: boolean
  release:
    types: [published]

permissions:
  contents: read
  packages: write

env:
  GHCR_IMAGE: ghcr.io/lfnovo/open-notebook
  DOCKERHUB_IMAGE: lfnovo/open_notebook

jobs:
  extract-version:
    runs-on: ubuntu-latest
    outputs:
      version: ${{ steps.version.outputs.version }}
      has_dockerhub_secrets: ${{ steps.check.outputs.has_dockerhub_secrets }}
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Extract version from pyproject.toml
        id: version
        run: |
          VERSION=$(grep -m1 '^version = ' pyproject.toml | cut -d'"' -f2)
          echo "version=$VERSION" >> $GITHUB_OUTPUT
          echo "Extracted version: $VERSION"

      - name: Check for Docker Hub credentials
        id: check
        env:
          SECRET_DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
          SECRET_DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
        run: |
          if [[ -n ""$SECRET_DOCKER_USERNAME"" && -n ""$SECRET_DOCKER_PASSWORD"" ]]; then
            echo "has_dockerhub_secrets=true" >> $GITHUB_OUTPUT
            echo "Docker Hub credentials available"
          else
            echo "has_dockerhub_secrets=false" >> $GITHUB_OUTPUT
            echo "Docker Hub credentials not available - will only push to GHCR"
          fi

  build-regular:
    needs: extract-version
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Free up disk space
        run: |
          sudo rm -rf /usr/share/dotnet
          sudo rm -rf /usr/local/lib/android
          sudo rm -rf /opt/ghc
          sudo rm -rf /opt/hostedtoolcache/CodeQL
          sudo docker image prune --all --force
          df -h

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to GitHub Container Registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Login to Docker Hub
        if: needs.extract-version.outputs.has_dockerhub_secrets == 'true'
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      - name: Cache Docker layers
        uses: actions/cache@v3
        with:
          path: /tmp/.buildx-cache
          key: ${{ runner.os }}-buildx-regular-${{ github.sha }}
          restore-keys: |
            ${{ runner.os }}-buildx-regular-

      - name: Prepare Docker tags for regular build
        id: tags-regular
        env:
          ENV_GHCR_IMAGE: ${{ env.GHCR_IMAGE }}
          GITHUB_EVENT_INPUTS_PUSH_LATEST: ${{ github.event.inputs.push_latest }}
          GITHUB_EVENT_NAME: ${{ github.event_name }}
          GITHUB_EVENT_RELEASE_PRERELEASE: ${{ github.event.release.prerelease }}
          ENV_DOCKERHUB_IMAGE: ${{ env.DOCKERHUB_IMAGE }}
        run: |
          TAGS=""$ENV_GHCR_IMAGE":${{ needs.extract-version.outputs.version }}"

          # Determine if we should push latest tags
          PUSH_LATEST=""$GITHUB_EVENT_INPUTS_PUSH_LATEST""
          if [[ -z "$PUSH_LATEST" ]]; then
            PUSH_LATEST="false"
          fi

          # Add GHCR latest tag if requested or for non-prerelease releases
          if [[ "$PUSH_LATEST" == "true" ]] || [[ ""$GITHUB_EVENT_NAME"" == "release" && ""$GITHUB_EVENT_RELEASE_PRERELEASE"" != "true" ]]; then
            TAGS="${TAGS},"$ENV_GHCR_IMAGE":v1-latest"
          fi

          # Add Docker Hub tags if credentials available
          if [[ "${{ needs.extract-version.outputs.has_dockerhub_secrets }}" == "true" ]]; then
            TAGS="${TAGS},"$ENV_DOCKERHUB_IMAGE":${{ needs.extract-version.outputs.version }}"

            if [[ "$PUSH_LATEST" == "true" ]] || [[ ""$GITHUB_EVENT_NAME"" == "release" && ""$GITHUB_EVENT_RELEASE_PRERELEASE"" != "true" ]]; then
              TAGS="${TAGS},"$ENV_DOCKERHUB_IMAGE":v1-latest"
            fi
          fi

          echo "tags=${TAGS}" >> $GITHUB_OUTPUT
          echo "Generated tags: ${TAGS}"

      - name: Build and push regular image
        uses: docker/build-push-action@v5
        with:
          context: .
          file: ./Dockerfile
          platforms: linux/amd64,linux/arm64
          push: true
          tags: ${{ steps.tags-regular.outputs.tags }}
          cache-from: type=local,src=/tmp/.buildx-cache
          cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max

      - name: Move cache
        run: |
          rm -rf /tmp/.buildx-cache
          mv /tmp/.buildx-cache-new /tmp/.buildx-cache

  build-single:
    needs: extract-version
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Free up disk space
        run: |
          sudo rm -rf /usr/share/dotnet
          sudo rm -rf /usr/local/lib/android
          sudo rm -rf /opt/ghc
          sudo rm -rf /opt/hostedtoolcache/CodeQL
          sudo docker image prune --all --force
          df -h

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to GitHub Container Registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Login to Docker Hub
        if: needs.extract-version.outputs.has_dockerhub_secrets == 'true'
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      - name: Cache Docker layers
        uses: actions/cache@v3
        with:
          path: /tmp/.buildx-cache-single
          key: ${{ runner.os }}-buildx-single-${{ github.sha }}
          restore-keys: |
            ${{ runner.os }}-buildx-single-

      - name: Prepare Docker tags for single build
        id: tags-single
        env:
          ENV_GHCR_IMAGE: ${{ env.GHCR_IMAGE }}
          GITHUB_EVENT_INPUTS_PUSH_LATEST: ${{ github.event.inputs.push_latest }}
          GITHUB_EVENT_NAME: ${{ github.event_name }}
          GITHUB_EVENT_RELEASE_PRERELEASE: ${{ github.event.release.prerelease }}
          ENV_DOCKERHUB_IMAGE: ${{ env.DOCKERHUB_IMAGE }}
        run: |
          TAGS=""$ENV_GHCR_IMAGE":${{ needs.extract-version.outputs.version }}-single"

          # Determine if we should push latest tags
          PUSH_LATEST=""$GITHUB_EVENT_INPUTS_PUSH_LATEST""
          if [[ -z "$PUSH_LATEST" ]]; then
            PUSH_LATEST="false"
          fi

          # Add GHCR latest tag if requested or for non-prerelease releases
          if [[ "$PUSH_LATEST" == "true" ]] || [[ ""$GITHUB_EVENT_NAME"" == "release" && ""$GITHUB_EVENT_RELEASE_PRERELEASE"" != "true" ]]; then
            TAGS="${TAGS},"$ENV_GHCR_IMAGE":v1-latest-single"
          fi

          # Add Docker Hub tags if credentials available
          if [[ "${{ needs.extract-version.outputs.has_dockerhub_secrets }}" == "true" ]]; then
            TAGS="${TAGS},"$ENV_DOCKERHUB_IMAGE":${{ needs.extract-version.outputs.version }}-single"

            if [[ "$PUSH_LATEST" == "true" ]] || [[ ""$GITHUB_EVENT_NAME"" == "release" && ""$GITHUB_EVENT_RELEASE_PRERELEASE"" != "true" ]]; then
              TAGS="${TAGS},"$ENV_DOCKERHUB_IMAGE":v1-latest-single"
            fi
          fi

          echo "tags=${TAGS}" >> $GITHUB_OUTPUT
          echo "Generated tags: ${TAGS}"

      - name: Build and push single-container image
        uses: docker/build-push-action@v5
        with:
          context: .
          file: ./Dockerfile.single
          platforms: linux/amd64,linux/arm64
          push: true
          tags: ${{ steps.tags-single.outputs.tags }}
          cache-from: type=local,src=/tmp/.buildx-cache-single
          cache-to: type=local,dest=/tmp/.buildx-cache-single-new,mode=max

      - name: Move cache
        run: |
          rm -rf /tmp/.buildx-cache-single
          mv /tmp/.buildx-cache-single-new /tmp/.buildx-cache-single

  summary:
    needs: [extract-version, build-regular, build-single]
    runs-on: ubuntu-latest
    if: always()
    steps:
      - name: Build Summary
        env:
          GITHUB_EVENT_INPUTS_PUSH_LATEST_____FALSE_: ${{ github.event.inputs.push_latest || 'false' }}
          ENV_GHCR_IMAGE: ${{ env.GHCR_IMAGE }}
          ENV_DOCKERHUB_IMAGE: ${{ env.DOCKERHUB_IMAGE }}
          GITHUB_EVENT_INPUTS_PUSH_LATEST: ${{ github.event.inputs.push_latest }}
        run: |
          echo "## Build Summary" >> $GITHUB_STEP_SUMMARY
          echo "**Version:** ${{ needs.extract-version.outputs.version }}" >> $GITHUB_STEP_SUMMARY
          echo "**Push v1-Latest:** "$GITHUB_EVENT_INPUTS_PUSH_LATEST_____FALSE_"" >> $GITHUB_STEP_SUMMARY
          echo "" >> $GITHUB_STEP_SUMMARY
          echo "### Registries:" >> $GITHUB_STEP_SUMMARY
          echo "✅ **GHCR:** \`"$ENV_GHCR_IMAGE"\`" >> $GITHUB_STEP_SUMMARY
          if [[ "${{ needs.extract-version.outputs.has_dockerhub_secrets }}" == "true" ]]; then
            echo "✅ **Docker Hub:** \`"$ENV_DOCKERHUB_IMAGE"\`" >> $GITHUB_STEP_SUMMARY
          else
            echo "⏭️ **Docker Hub:** Skipped (credentials not configured)" >> $GITHUB_STEP_SUMMARY
          fi
          echo "" >> $GITHUB_STEP_SUMMARY
          echo "### Images Built:" >> $GITHUB_STEP_SUMMARY

          if [[ "${{ needs.build-regular.result }}" == "success" ]]; then
            echo "✅ **Regular (GHCR):** \`"$ENV_GHCR_IMAGE":${{ needs.extract-version.outputs.version }}\`" >> $GITHUB_STEP_SUMMARY
            if [[ ""$GITHUB_EVENT_INPUTS_PUSH_LATEST"" == "true" ]]; then
              echo "✅ **Regular v1-Latest (GHCR):** \`"$ENV_GHCR_IMAGE":v1-latest\`" >> $GITHUB_STEP_SUMMARY
            fi
            if [[ "${{ needs.extract-version.outputs.has_dockerhub_secrets }}" == "true" ]]; then
              echo "✅ **Regular (Docker Hub):** \`"$ENV_DOCKERHUB_IMAGE":${{ needs.extract-version.outputs.version }}\`" >> $GITHUB_STEP_SUMMARY
              if [[ ""$GITHUB_EVENT_INPUTS_PUSH_LATEST"" == "true" ]]; then
                echo "✅ **Regular v1-Latest (Docker Hub):** \`"$ENV_DOCKERHUB_IMAGE":v1-latest\`" >> $GITHUB_STEP_SUMMARY
              fi
            fi
          elif [[ "${{ needs.build-regular.result }}" == "skipped" ]]; then
            echo "⏭️ **Regular:** Skipped" >> $GITHUB_STEP_SUMMARY
          else
            echo "❌ **Regular:** Failed" >> $GITHUB_STEP_SUMMARY
          fi

          if [[ "${{ needs.build-single.result }}" == "success" ]]; then
            echo "✅ **Single (GHCR):** \`"$ENV_GHCR_IMAGE":${{ needs.extract-version.outputs.version }}-single\`" >> $GITHUB_STEP_SUMMARY
            if [[ ""$GITHUB_EVENT_INPUTS_PUSH_LATEST"" == "true" ]]; then
              echo "✅ **Single v1-Latest (GHCR):** \`"$ENV_GHCR_IMAGE":v1-latest-single\`" >> $GITHUB_STEP_SUMMARY
            fi
            if [[ "${{ needs.extract-version.outputs.has_dockerhub_secrets }}" == "true" ]]; then
              echo "✅ **Single (Docker Hub):** \`"$ENV_DOCKERHUB_IMAGE":${{ needs.extract-version.outputs.version }}-single\`" >> $GITHUB_STEP_SUMMARY
              if [[ ""$GITHUB_EVENT_INPUTS_PUSH_LATEST"" == "true" ]]; then
                echo "✅ **Single v1-Latest (Docker Hub):** \`"$ENV_DOCKERHUB_IMAGE":v1-latest-single\`" >> $GITHUB_STEP_SUMMARY
              fi
            fi
          elif [[ "${{ needs.build-single.result }}" == "skipped" ]]; then
            echo "⏭️ **Single:** Skipped" >> $GITHUB_STEP_SUMMARY
          else
            echo "❌ **Single:** Failed" >> $GITHUB_STEP_SUMMARY
          fi

          echo "" >> $GITHUB_STEP_SUMMARY
          echo "### Platforms:" >> $GITHUB_STEP_SUMMARY
          echo "- linux/amd64" >> $GITHUB_STEP_SUMMARY
          echo "- linux/arm64" >> $GITHUB_STEP_SUMMARY

================================================
FILE: .github/workflows/build-dev.yml
================================================
name: Development Build

on:
  pull_request:
    branches: [ main ]
  push:
    branches: [ main ]
    paths-ignore:
      - '**.md'
      - 'docs/**'
      - 'notebooks/**'
      - '.github/workflows/claude*.yml'
  workflow_dispatch:
    inputs:
      platform:
        description: 'Platform to build'
        required: true
        default: 'linux/amd64'
        type: choice
        options:
          - linux/amd64
          - linux/arm64
          - linux/amd64,linux/arm64

permissions:
  contents: read
  packages: write

env:
  GHCR_IMAGE: ghcr.io/lfnovo/open-notebook
  DOCKERHUB_IMAGE: lfnovo/open_notebook

jobs:
  extract-version:
    runs-on: ubuntu-latest
    outputs:
      version: ${{ steps.version.outputs.version }}
      has_dockerhub_secrets: ${{ steps.check.outputs.has_dockerhub_secrets }}
      is_push_to_main: ${{ steps.check.outputs.is_push_to_main }}
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Extract version from pyproject.toml
        id: version
        run: |
          VERSION=$(grep -m1 '^version = ' pyproject.toml | cut -d'"' -f2)
          echo "version=$VERSION" >> $GITHUB_OUTPUT
          echo "Extracted version: $VERSION"

      - name: Check environment
        id: check
        env:
          SECRET_DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
          SECRET_DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
        run: |
          # Check for Docker Hub credentials
          if [[ -n "$SECRET_DOCKER_USERNAME" && -n "$SECRET_DOCKER_PASSWORD" ]]; then
            echo "has_dockerhub_secrets=true" >> $GITHUB_OUTPUT
            echo "Docker Hub credentials available"
          else
            echo "has_dockerhub_secrets=false" >> $GITHUB_OUTPUT
            echo "Docker Hub credentials not available"
          fi

          # Check if this is a push to main (not a PR)
          if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" == "refs/heads/main" ]]; then
            echo "is_push_to_main=true" >> $GITHUB_OUTPUT
            echo "This is a push to main - will publish v1-dev tags"
          else
            echo "is_push_to_main=false" >> $GITHUB_OUTPUT
            echo "This is a PR or manual run - test build only"
          fi

  build-regular:
    needs: extract-version
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Free up disk space
        if: needs.extract-version.outputs.is_push_to_main == 'true'
        run: |
          sudo rm -rf /usr/share/dotnet
          sudo rm -rf /usr/local/lib/android
          sudo rm -rf /opt/ghc
          sudo rm -rf /opt/hostedtoolcache/CodeQL
          sudo docker image prune --all --force
          df -h

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to GitHub Container Registry
        if: needs.extract-version.outputs.is_push_to_main == 'true'
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Login to Docker Hub
        if: needs.extract-version.outputs.is_push_to_main == 'true' && needs.extract-version.outputs.has_dockerhub_secrets == 'true'
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      - name: Cache Docker layers
        uses: actions/cache@v3
        with:
          path: /tmp/.buildx-cache-dev
          key: ${{ runner.os }}-buildx-dev-${{ github.sha }}
          restore-keys: |
            ${{ runner.os }}-buildx-dev-

      - name: Prepare Docker tags
        id: tags
        run: |
          if [[ "${{ needs.extract-version.outputs.is_push_to_main }}" == "true" ]]; then
            # Push to main: build and push v1-dev tags
            TAGS="${{ env.GHCR_IMAGE }}:v1-dev"
            if [[ "${{ needs.extract-version.outputs.has_dockerhub_secrets }}" == "true" ]]; then
              TAGS="${TAGS},${{ env.DOCKERHUB_IMAGE }}:v1-dev"
            fi
            echo "tags=${TAGS}" >> $GITHUB_OUTPUT
            echo "push=true" >> $GITHUB_OUTPUT
            echo "platforms=linux/amd64,linux/arm64" >> $GITHUB_OUTPUT
          else
            # PR or manual: test build only
            echo "tags=${{ env.DOCKERHUB_IMAGE }}:${{ needs.extract-version.outputs.version }}-dev" >> $GITHUB_OUTPUT
            echo "push=false" >> $GITHUB_OUTPUT
            echo "platforms=${{ github.event.inputs.platform || 'linux/amd64' }}" >> $GITHUB_OUTPUT
          fi

      - name: Build and push regular image
        uses: docker/build-push-action@v5
        with:
          context: .
          file: ./Dockerfile
          platforms: ${{ steps.tags.outputs.platforms }}
          push: ${{ steps.tags.outputs.push }}
          tags: ${{ steps.tags.outputs.tags }}
          cache-from: type=local,src=/tmp/.buildx-cache-dev
          cache-to: type=local,dest=/tmp/.buildx-cache-dev-new,mode=max

      - name: Move cache
        run: |
          rm -rf /tmp/.buildx-cache-dev
          mv /tmp/.buildx-cache-dev-new /tmp/.buildx-cache-dev

  build-single:
    needs: extract-version
    # Only build single image on push to main
    if: needs.extract-version.outputs.is_push_to_main == 'true'
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Free up disk space
        run: |
          sudo rm -rf /usr/share/dotnet
          sudo rm -rf /usr/local/lib/android
          sudo rm -rf /opt/ghc
          sudo rm -rf /opt/hostedtoolcache/CodeQL
          sudo docker image prune --all --force
          df -h

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to GitHub Container Registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Login to Docker Hub
        if: needs.extract-version.outputs.has_dockerhub_secrets == 'true'
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      - name: Cache Docker layers
        uses: actions/cache@v3
        with:
          path: /tmp/.buildx-cache-dev-single
          key: ${{ runner.os }}-buildx-dev-single-${{ github.sha }}
          restore-keys: |
            ${{ runner.os }}-buildx-dev-single-

      - name: Prepare Docker tags
        id: tags
        run: |
          TAGS="${{ env.GHCR_IMAGE }}:v1-dev-single"
          if [[ "${{ needs.extract-version.outputs.has_dockerhub_secrets }}" == "true" ]]; then
            TAGS="${TAGS},${{ env.DOCKERHUB_IMAGE }}:v1-dev-single"
          fi
          echo "tags=${TAGS}" >> $GITHUB_OUTPUT

      - name: Build and push single-container image
        uses: docker/build-push-action@v5
        with:
          context: .
          file: ./Dockerfile.single
          platforms: linux/amd64,linux/arm64
          push: true
          tags: ${{ steps.tags.outputs.tags }}
          cache-from: type=local,src=/tmp/.buildx-cache-dev-single
          cache-to: type=local,dest=/tmp/.buildx-cache-dev-single-new,mode=max

      - name: Move cache
        run: |
          rm -rf /tmp/.buildx-cache-dev-single
          mv /tmp/.buildx-cache-dev-single-new /tmp/.buildx-cache-dev-single

  summary:
    needs: [extract-version, build-regular, build-single]
    runs-on: ubuntu-latest
    if: always()
    steps:
      - name: Development Build Summary
        run: |
          echo "## Development Build Summary" >> $GITHUB_STEP_SUMMARY
          echo "**Version:** ${{ needs.extract-version.outputs.version }}" >> $GITHUB_STEP_SUMMARY
          echo "**Event:** ${{ github.event_name }}" >> $GITHUB_STEP_SUMMARY
          echo "**Push to Main:** ${{ needs.extract-version.outputs.is_push_to_main }}" >> $GITHUB_STEP_SUMMARY
          echo "" >> $GITHUB_STEP_SUMMARY

          if [[ "${{ needs.extract-version.outputs.is_push_to_main }}" == "true" ]]; then
            echo "### Published Tags:" >> $GITHUB_STEP_SUMMARY

            if [[ "${{ needs.build-regular.result }}" == "success" ]]; then
              echo "✅ **Regular:** \`${{ env.GHCR_IMAGE }}:v1-dev\`" >> $GITHUB_STEP_SUMMARY
              if [[ "${{ needs.extract-version.outputs.has_dockerhub_secrets }}" == "true" ]]; then
                echo "✅ **Regular (Docker Hub):** \`${{ env.DOCKERHUB_IMAGE }}:v1-dev\`" >> $GITHUB_STEP_SUMMARY
              fi
            else
              echo "❌ **Regular:** Build failed" >> $GITHUB_STEP_SUMMARY
            fi

            if [[ "${{ needs.build-single.result }}" == "success" ]]; then
              echo "✅ **Single:** \`${{ env.GHCR_IMAGE }}:v1-dev-single\`" >> $GITHUB_STEP_SUMMARY
              if [[ "${{ needs.extract-version.outputs.has_dockerhub_secrets }}" == "true" ]]; then
                echo "✅ **Single (Docker Hub):** \`${{ env.DOCKERHUB_IMAGE }}:v1-dev-single\`" >> $GITHUB_STEP_SUMMARY
              fi
            elif [[ "${{ needs.build-single.result }}" == "skipped" ]]; then
              echo "⏭️ **Single:** Skipped" >> $GITHUB_STEP_SUMMARY
            else
              echo "❌ **Single:** Build failed" >> $GITHUB_STEP_SUMMARY
            fi

            echo "" >> $GITHUB_STEP_SUMMARY
            echo "### Platforms:" >> $GITHUB_STEP_SUMMARY
            echo "- linux/amd64" >> $GITHUB_STEP_SUMMARY
            echo "- linux/arm64" >> $GITHUB_STEP_SUMMARY
          else
            echo "### Test Build Results:" >> $GITHUB_STEP_SUMMARY
            if [[ "${{ needs.build-regular.result }}" == "success" ]]; then
              echo "✅ **Dockerfile:** Build successful" >> $GITHUB_STEP_SUMMARY
            else
              echo "❌ **Dockerfile:** Build failed" >> $GITHUB_STEP_SUMMARY
            fi
            echo "" >> $GITHUB_STEP_SUMMARY
            echo "### Notes:" >> $GITHUB_STEP_SUMMARY
            echo "- This is a test build (no images pushed to registry)" >> $GITHUB_STEP_SUMMARY
            echo "- Merge to main to publish \`v1-dev\` tags" >> $GITHUB_STEP_SUMMARY
            echo "- For stable releases, use the 'Build and Release' workflow" >> $GITHUB_STEP_SUMMARY
          fi


================================================
FILE: .github/workflows/claude-code-review.yml
================================================
name: Claude Code Review

on:
  pull_request:
    types: [opened, synchronize, ready_for_review, reopened]
  pull_request_target:
    types: [opened, synchronize, ready_for_review, reopened]
    # Optional: Only run on specific file changes
    # paths:
    #   - "src/**/*.ts"
    #   - "src/**/*.tsx"
    #   - "src/**/*.js"
    #   - "src/**/*.jsx"

jobs:
  claude-review:
    # Run for fork PRs (via pull_request_target) OR same-repo PRs (via pull_request), but not both
    if: |
      (github.event_name == 'pull_request_target' && github.event.pull_request.head.repo.full_name != github.repository) ||
      (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository)

    runs-on: ubuntu-latest
    permissions:
      contents: read
      pull-requests: write
      issues: write
      id-token: write

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha }}
          fetch-depth: 1
          persist-credentials: false

      - name: Run Claude Code Review
        id: claude-review
        uses: anthropics/claude-code-action@v1
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
          plugin_marketplaces: 'https://github.com/anthropics/claude-code.git'
          plugins: 'code-review@claude-code-plugins'
          prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}'
          # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
          # or https://code.claude.com/docs/en/cli-reference for available options



================================================
FILE: .github/workflows/claude.yml
================================================
name: Claude Code

on:
  issue_comment:
    types: [created]
  pull_request_review_comment:
    types: [created]
  issues:
    types: [opened, assigned]
  pull_request_review:
    types: [submitted]

jobs:
  claude:
    if: |
      (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
      (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
      (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
      (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pull-requests: write
      issues: write
      id-token: write
      actions: read # Required for Claude to read CI results on PRs
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          fetch-depth: 1

      - name: Run Claude Code
        id: claude
        uses: anthropics/claude-code-action@v1
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}

          # This is an optional setting that allows Claude to read CI results on PRs
          additional_permissions: |
            actions: read

          # Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.
          # prompt: 'Update the pull request description to include a summary of changes.'

          # Optional: Add claude_args to customize behavior and configuration
          # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
          # or https://code.claude.com/docs/en/cli-reference for available options
          # claude_args: '--allowed-tools Bash(gh pr:*)'



================================================
FILE: .github/workflows/test.yml
================================================
name: Tests

on:
  pull_request:
    branches: [main]
  push:
    branches: [main]
    paths-ignore:
      - '**.md'
      - 'docs/**'
      - '.github/workflows/claude*.yml'

permissions:
  contents: read

jobs:
  backend:
    name: Backend Tests
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Set up uv
        uses: astral-sh/setup-uv@v4
        with:
          enable-cache: true

      - name: Set up Python
        run: uv python install

      - name: Install dependencies
        run: uv sync

      - name: Run tests
        run: uv run pytest tests/ -v

  frontend:
    name: Frontend Tests
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: frontend
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm
          cache-dependency-path: frontend/package-lock.json

      - name: Install dependencies
        run: npm ci

      - name: Run tests
        run: npm test


================================================
FILE: .gitignore
================================================
.env
prompts/patterns/user/
/notebooks/
data/
.uploads/
sqlite-db/
surreal-data/
docker.env
notebook_data/
# Python-specific
*.py[cod]
__pycache__/
*.so
todo.md
temp/
google-credentials.json
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
/lib/
/lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg

# PyInstaller
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/

# Jupyter Notebook
.ipynb_checkpoints

# IPython
profile_default/
ipython_config.py

# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# PyCharm
.idea/

# VS Code
.vscode/

# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# mkdocs documentation
/site

# mypy
.mypy_cache/
.dmypy.json
dmypy.json

# Pyre type checker
.pyre/

# pytype static type analyzer
.pytype/

# Cython debug symbols
cython_debug/

# macOS
.DS_Store

# Windows
Thumbs.db
ehthumbs.db
desktop.ini

# Linux
*~

# Log files
*.log

# Database files
*.db
*.sqlite3

.quarentena

claude-logs/
.claude/sessions
**/claude-logs


docs/custom_gpt
doc_exports/

specs/
.claude
.sisyphus

.playwright-mcp/



*.local.yml
**/*.local.md

================================================
FILE: .python-version
================================================
3.12


================================================
FILE: .worktreeinclude
================================================
.env
.env.local
.env.*
**/.claude/settings.local.json
CLAUDE.local.md


================================================
FILE: CHANGELOG.md
================================================
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [1.8.1] - 2026-03-10

### Added
- i18n support for Bengali (bn-IN) (#643)
- Podcast language support via podcast-creator 0.12.0 (#645)
- Upgrade default Azure API version for model testing and fetching (#638)

### Fixed
- Tiktoken network errors in offline/air-gapped Docker deployments — pre-downloads encoding at build time (#264, #622)
- SurrealDB getting stuck (#656)

### Dependencies
- Bump esperanto to 2.19.5 (#657)
- Bump langgraph from 1.0.6 to 1.0.10rc1 (#658)
- Bump authlib from 1.6.6 to 1.6.7 (#649)
- Bump lxml-html-clean from 0.4.3 to 0.4.4 (#646)
- Bump rollup from 4.55.1 to 4.59.0 (#635)
- Bump minimatch in frontend (#634)
- Bump tar from 7.5.9 to 7.5.11 (#650, #659)

## [1.7.4] - 2026-02-18

### Fixed
- Embedding large documents (3MB+) fails with 413 Payload Too Large (#594)
- `generate_embeddings()` now batches texts in groups of 50 with per-batch retry, preventing provider payload limits from being exceeded
- 413 errors now classified with user-friendly message in error classifier
- Misleading "Created 0 embedded chunks" log in `process_source_command` — embedding is fire-and-forget, so the count was always 0; now logs "embedding submitted" instead

## [1.7.3] - 2026-02-17

### Added
- Retry button for failed podcast episodes in the UI (#211, #218)
- Error details displayed on failed podcast episodes (#185, #355)
- `POST /podcasts/episodes/{id}/retry` API endpoint for re-submitting failed episodes
- `error_message` field in podcast episode API responses

### Fixed
- Podcast generation failures now correctly marked as "failed" instead of "completed" (#300, #335)
- Disabled automatic retries for podcast generation to prevent duplicate episode records (#302)

### Dependencies
- Bump podcast-creator to >= 0.11.2
- Bump esperanto to >= 2.19.4

## [1.7.2] - 2026-02-16

### Added
- Error classification utility that maps LLM provider errors to user-friendly messages (#506)
- Global exception handlers in FastAPI for all custom exception types with proper HTTP status codes
- `getApiErrorMessage()` frontend helper that falls back to backend messages when no i18n mapping exists

### Fixed
- LLM errors (invalid API key, wrong model, rate limits) now show descriptive messages instead of "An unexpected error occurred" (#590)
- SSE streaming error events in source chat and ask hooks were swallowed by inner JSON parse catch blocks
- Transformation execution errors were caught and re-wrapped as generic 500s instead of using proper status codes
- Fail fast when source content extraction returns empty instead of retrying (#589)
- Chat input and message overflow with long unbroken strings (#588)
- Word-wrap overflow in source cards, note editor, inline edit, note titles, and dialog content (#588)
- Translation proxy shadowing `name` keys (#588)
- OpenAI-compatible provider name handling via Esperanto update (#583)

### Changed
- `ValueError` replaced with `ConfigurationError` in model provisioning for proper error classification
- `ConfigurationError` added to command retry `stop_on` lists to avoid retrying permanent config failures

### Dependencies
- Bump esperanto to 2.19.3 (#583)
- Bump podcast-creator to 0.9.1

## [1.7.1] - 2026-02-14

### Added
- French (fr-FR) language support (#581)
- CI test workflow and improved i18n validation (#580)
- Expose embed `command_id` in note API responses (#545)

### Fixed
- ElevenLabs TTS credential passthrough via Esperanto update (#578)
- Handle empty/whitespace source content without retry loop (#576)
- Increase transformation `max_tokens` and update Esperanto dep (#568)
- Turn the embedding field into optional (#557)

### Docs
- Fix docker container names in local setup guides (#577)

### Dependencies
- Bump langchain-core from 1.2.7 to 1.2.11 (#564)
- Bump cryptography from 46.0.3 to 46.0.5 (#563)

## [1.7.0] - 2026-02-10

### Added
- **Credential-Based Provider Management** (#477)
  - New Settings → API Keys page for managing AI provider credentials via the UI
  - Support for 14 providers: OpenAI, Anthropic, Google, Groq, Mistral, DeepSeek, xAI, OpenRouter, Voyage AI, ElevenLabs, Ollama, Azure OpenAI, OpenAI-Compatible, and Vertex AI
  - Secure storage of API keys in SurrealDB with field-level encryption (Fernet AES-128-CBC + HMAC-SHA256)
  - One-click connection testing, model discovery, and model registration per credential
  - Migration tool to import existing environment variable keys into the credential system
  - Azure OpenAI support with service-specific endpoints (LLM, Embedding, STT, TTS)
  - OpenAI-Compatible support with per-service URL configurations
  - Vertex AI support with project, location, and credentials path
  - Environment variable API keys deprecated in favor of Settings UI

- **Security Enhancements**
  - Docker secrets support via `_FILE` suffix pattern (e.g., `OPEN_NOTEBOOK_PASSWORD_FILE`)
  - Default encryption key derived from "0p3n-N0t3b0ok" for easy setup (change in production!)
  - Default password "open-notebook-change-me" for out-of-box experience (change in production!)
  - URL validation for SSRF protection - blocks private IPs and localhost (except for Ollama which runs locally)
  - Security warnings logged when using default credentials

- HTML clipboard detection for text sources (#426)
  - When pasting content, automatically detects HTML format (e.g., from Word, web pages)
  - Shows info message when HTML is detected, informing user it will be converted to Markdown
  - Preserves formatting that would be lost with plain text paste
  - Bump content-core to 0.11.0 for HTML to Markdown conversion support

- **Improved Getting Started Experience**
  - Simplified docker-compose.yml in repository root (single official file)
  - Added examples/ folder with ready-made configurations:
    - `docker-compose-ollama.yml` - Local AI with Ollama
    - `docker-compose-speaches.yml` - Local TTS/STT with Speaches
    - `docker-compose-full-local.yml` - 100% local setup (Ollama + Speaches)
  - Inline quick start in README (no need to navigate to docs)
  - Cross-references between docker-compose examples and documentation
  - .env.example template with all configuration options

### Fixed
- Azure form race condition: all configuration now saved in single atomic request
- Migration API "error error" display: added proper MigrationResult model with message field
- Connection tester for Ollama providers: improved error handling and URL validation
- SqliteSaver async compatibility issues in chat system (#509, #525, #538)
- Re-embedding failures with empty content (#513, #515)
- Deletion cascade for notes and sources (#77)
- YouTube content availability issues (#494)
- Large document embedding errors (#489)

### Security
- API keys are encrypted at rest using Fernet symmetric encryption
- Keys are never returned to the frontend, only configuration status
- SSRF protection prevents internal network access via URL validation

### Docs
- Complete documentation update for credential-based system across 25 files
- All quick-start, installation, and configuration guides now use Settings UI workflow
- Environment variable API key instructions moved to deprecated/legacy sections
- Fixed broken links in installation docs
- Added comprehensive examples/ folder with documented docker-compose configurations
- Updated local-tts.md and local-stt.md with links to ready-made examples

### Internationalization
- Added Russian (ru-RU) language support (#524)
- Added Italian (it-IT) language support (#508)

## [1.6.2] - 2026-01-24

### Fixed
- Connection error with llama.cpp and OpenAI-compatible providers (#465)
  - Bump Esperanto to 2.17.2 which fixes LangChain connection errors caused by garbage collection

## [1.6.1] - 2026-01-22

### Fixed
- "Failed to send message" error with unhelpful logs when chat model is not configured (#358)
  - Added detailed error logging with model selection context and full traceback
  - Improved error messages to guide users to Settings → Models
  - Added warnings when default models are not configured

### Docs
- Ollama troubleshooting: Added "Model Name Configuration" section emphasizing exact model names from `ollama list`
- Added troubleshooting entry for "Failed to send message" error with step-by-step solutions
- Updated AI Chat Issues documentation with model configuration guidance


## [1.6.0] - 2026-01-21

### Added
- Content-type aware text chunking with automatic HTML, Markdown, and plain text detection (#350, #142)
- Unified embedding generation with mean pooling for large content that exceeds model context limits
- Dedicated embedding commands: `embed_note`, `embed_insight`, `embed_source`
- New utility modules: `chunking.py` and `embedding.py` in `open_notebook/utils/`
- Japanese (ja-JP) language support (#450)

### Changed
- Embedding is now fire-and-forget: domain models submit embedding commands asynchronously after save
- `rebuild_embeddings_command` now delegates to individual embed_* commands instead of inline processing
- Chunk size reduced to 1500 characters for better compatibility with Ollama embedding models
- Bump Esperanto to 2.16 for increased Ollama context window support

### Removed
- Legacy embedding commands: `embed_single_item_command`, `embed_chunk_command`, `vectorize_source_command`
- `needs_embedding()` and `get_embedding_content()` methods from domain models
- `split_text()` function from text_utils (replaced by `chunk_text()` in chunking module)

### Fixed
- Embedding failures when content exceeds model context limits (#350, #142)
- Empty note titles when saving from chat (clean thinking tags from prompt graph output)
- Orphaned embedding/insight records when deleting sources (cascade delete)
- Search results crash with null parent_id (defensive frontend check)
- Database migration 10 cleans up existing orphaned records

## [1.5.2] - 2026-01-15

### Performance
- Improved source listing speed by 20-30x (#436, closes #351)
  - Added database indexes on `source` field for `source_insight` and `source_embedding` tables
  - Use SurrealDB `FETCH` clause for command status instead of N async calls

## [1.5.1] - 2026-01-15

### Fixed
- Podcast dialog infinite loop error caused by excessive translation Proxy accesses in loops
- Podcast dialog UI freezing when typing episode name or additional instructions
- Removed incorrect translation keys for user-defined episode profiles (user content should not be translated)

## [1.5.0] - 2026-01-15

### Added
- Internationalization (i18n) support with Chinese (Simplified and Traditional) translations (#371, closes #344, #349, #360)
- Frontend test infrastructure with Vitest (#371)
- Language toggle component for switching UI language (#371)
- Date localization using date-fns locales (#371)
- Error message translation system (#371)

### Fixed
- Accessibility improvements: added missing `id`, `name`, and `autoComplete` attributes to form inputs (#371)
- Added `DialogDescription` to dialogs for Radix UI accessibility compliance (#371)
- Fixed "Collapsible is changing from uncontrolled to controlled" warning in SettingsForm (#371)
- Fixed lint command for Next.js 16 compatibility (`eslint` instead of `next lint`)

### Changed
- Dockerfile optimizations: better layer caching, `--no-install-recommends` for smaller images (#371)
- Dockerfile.single refactored into 3 separate build stages for better caching (#371)

## [1.4.0] - 2026-01-14

### Added
- CTA button to empty state notebook list for better onboarding (#408)
- Offline deployment support for Docker containers (#414)

### Fixed
- Large file uploads (>10MB) by upgrading to Next.js 16 (#423)
- Orphaned uploaded files when sources are removed (#421)
- Broken documentation links to ai-providers.md (#419)
- ZIP support indication removed from UI (#418)
- Duplicate Claude Code workflow runs on PRs (#417)
- Claude Code review workflow now runs on PRs from forks (#416)

### Changed
- Upgraded Next.js from 15.4.10 to 16.1.1 (#423)
- Upgraded React from 19.1.0 to 19.2.3 (#423)
- Renamed `middleware.ts` to `proxy.ts` for Next.js 16 compatibility (#423)

### Dependencies
- next: 15.4.10 → 16.1.1
- react: 19.1.0 → 19.2.3
- react-dom: 19.1.0 → 19.2.3

## [1.2.4] - 2025-12-14

### Added
- Infinite scroll for notebook sources - no more 50 source limit (#325)
- Markdown table rendering in chat responses, search results, and insights (#325)

### Fixed
- Timeout errors with Ollama and local LLMs - increased to 10 minutes (#325)
- "Unable to Connect to API Server" on Docker startup - frontend now waits for API health check (#325, #315)
- SSL issues with langchain (#274)
- Query key consistency for source mutations to properly refresh infinite scroll (#325)
- Docker compose start-all flow (#323)

### Changed
- Timeout configuration now uses granular httpx.Timeout (short connect, long read) (#325)

### Dependencies
- Updated next.js to 15.4.10
- Updated httpx to >=0.27.0 for SSL fix


================================================
FILE: CLAUDE.md
================================================
# Open Notebook - Root CLAUDE.md

This file provides architectural guidance for contributors working on Open Notebook at the project level.

## Project Overview

**Open Notebook** is an open-source, privacy-focused alternative to Google's Notebook LM. It's an AI-powered research assistant enabling users to upload multi-modal content (PDFs, audio, video, web pages), generate intelligent notes, search semantically, chat with AI models, and produce professional podcasts—all with complete control over data and choice of AI providers.

**Key Values**: Privacy-first, multi-provider AI support, fully self-hosted option, open-source transparency.

---

## Three-Tier Architecture

```
┌─────────────────────────────────────────────────────────┐
│              Frontend (React/Next.js)                    │
│              frontend/ @ port 3000                       │
├─────────────────────────────────────────────────────────┤
│ - Notebooks, sources, notes, chat, podcasts, search UI  │
│ - Zustand state management, TanStack Query (React Query)│
│ - Shadcn/ui component library with Tailwind CSS         │
└────────────────────────┬────────────────────────────────┘
                         │ HTTP REST
┌────────────────────────▼────────────────────────────────┐
│              API (FastAPI)                              │
│              api/ @ port 5055                           │
├─────────────────────────────────────────────────────────┤
│ - REST endpoints for notebooks, sources, notes, chat    │
│ - LangGraph workflow orchestration                      │
│ - Job queue for async operations (podcasts)             │
│ - Multi-provider AI provisioning via Esperanto          │
└────────────────────────┬────────────────────────────────┘
                         │ SurrealQL
┌────────────────────────▼────────────────────────────────┐
│         Database (SurrealDB)                            │
│         Graph database @ port 8000                      │
├─────────────────────────────────────────────────────────┤
│ - Records: Notebook, Source, Note, ChatSession, Credential│
│ - Relationships: source-to-notebook, note-to-source     │
│ - Vector embeddings for semantic search                 │
└─────────────────────────────────────────────────────────┘
```

---

## Useful sources

User documentation is at @docs/

## Tech Stack

### Frontend (`frontend/`)
- **Framework**: Next.js 16 (React 19)
- **Language**: TypeScript
- **State Management**: Zustand
- **Data Fetching**: TanStack Query (React Query)
- **Styling**: Tailwind CSS + Shadcn/ui
- **Build Tool**: Webpack (via Next.js)
- **i18n compatible**: All front-end changes must also consider the translation keys

### API Backend (`api/` + `open_notebook/`)
- **Framework**: FastAPI 0.104+
- **Language**: Python 3.11+
- **Workflows**: LangGraph state machines
- **Database**: SurrealDB async driver
- **AI Providers**: Esperanto library (8+ providers: OpenAI, Anthropic, Google, Groq, Ollama, Mistral, DeepSeek, xAI)
- **Job Queue**: Surreal-Commands for async jobs (podcasts)
- **Logging**: Loguru
- **Validation**: Pydantic v2
- **Testing**: Pytest

### Database
- **SurrealDB**: Graph database with built-in embedding storage and vector search
- **Schema Migrations**: Automatic on API startup via AsyncMigrationManager

### Additional Services
- **Content Processing**: content-core library (file/URL extraction)
- **Prompts**: AI-Prompter with Jinja2 templating
- **Podcast Generation**: podcast-creator library
- **Embeddings**: Multi-provider via Esperanto

---

## Architecture Highlights

### 1. Async-First Design
- All database queries, graph invocations, and API calls are async (await)
- SurrealDB async driver with connection pooling
- FastAPI handles concurrent requests efficiently

### 2. LangGraph Workflows
- **source.py**: Content ingestion (extract → embed → save)
- **chat.py**: Conversational agent with message history
- **ask.py**: Search + synthesis (retrieve relevant sources → LLM)
- **transformation.py**: Custom transformations on sources
- All use `provision_langchain_model()` for smart model selection

### 3. Multi-Provider AI
- **Esperanto library**: Unified interface to 8+ AI providers
- **Credential system**: Individual encrypted credential records per provider; models link to credentials for direct config
- **ModelManager**: Factory pattern with fallback logic; uses credential config when available, env vars as fallback
- **Smart selection**: Detects large contexts, prefers long-context models
- **Override support**: Per-request model configuration

### 4. Database Schema
- **Automatic migrations**: AsyncMigrationManager runs on API startup
- **SurrealDB graph model**: Records with relationships and embeddings
- **Vector search**: Built-in semantic search across all content
- **Transactions**: Repo functions handle ACID operations

### 5. Authentication
- **Current**: Simple password middleware (insecure, dev-only)
- **Production**: Replace with OAuth/JWT (see CONFIGURATION.md)

---

## Important Quirks & Gotchas

### API Startup
- **Migrations run automatically** on startup; check logs for errors
- **Must start API before UI**: UI depends on API for all data
- **SurrealDB must be running**: API fails without database connection

### Frontend-Backend Communication
- **Base API URL**: Configured in `.env.local` (default: http://localhost:5055)
- **CORS enabled**: Configured in `api/main.py` (allow all origins in dev)
- **Rate limiting**: Not built-in; add at proxy layer for production

### LangGraph Workflows
- **Blocking operations**: Chat/podcast workflows may take minutes; no timeout
- **State persistence**: Uses SQLite checkpoint storage in `/data/sqlite-db/`
- **Model fallback**: If primary model fails, falls back to cheaper/smaller model

### Podcast Generation
- **Async job queue**: `podcast_service.py` submits jobs but doesn't wait
- **Track status**: Use `/commands/{command_id}` endpoint to poll status
- **TTS failures**: Fall back to silent audio if speech synthesis fails

### Content Processing
- **File extraction**: Uses content-core library; supports 50+ file types
- **URL handling**: Extracts text + metadata from web pages
- **Large files**: Content processing is sync; may block API briefly

---

## Component References

See dedicated CLAUDE.md files for detailed guidance:

- **[frontend/CLAUDE.md](frontend/CLAUDE.md)**: React/Next.js architecture, state management, API integration
- **[api/CLAUDE.md](api/CLAUDE.md)**: FastAPI structure, service pattern, endpoint development
- **[open_notebook/CLAUDE.md](open_notebook/CLAUDE.md)**: Backend core, domain models, LangGraph workflows, AI provisioning
- **[open_notebook/domain/CLAUDE.md](open_notebook/domain/CLAUDE.md)**: Data models, repository pattern, search functions
- **[open_notebook/ai/CLAUDE.md](open_notebook/ai/CLAUDE.md)**: ModelManager, AI provider integration, Esperanto usage
- **[open_notebook/graphs/CLAUDE.md](open_notebook/graphs/CLAUDE.md)**: LangGraph workflow design, state machines
- **[open_notebook/database/CLAUDE.md](open_notebook/database/CLAUDE.md)**: SurrealDB operations, migrations, async patterns

---

## Documentation Map

- **[README.md](README.md)**: Project overview, features, quick start
- **[docs/index.md](docs/index.md)**: Complete user & deployment documentation
- **[CONFIGURATION.md](CONFIGURATION.md)**: Environment variables, model configuration
- **[CONTRIBUTING.md](CONTRIBUTING.md)**: Contribution guidelines
- **[MAINTAINER_GUIDE.md](MAINTAINER_GUIDE.md)**: Release & maintenance procedures

---

## Testing Strategy

- **Unit tests**: `tests/test_domain.py`, `test_models_api.py`
- **Graph tests**: `tests/test_graphs.py` (workflow integration)
- **Utils tests**: `tests/test_utils.py`, `tests/test_chunking.py`, `tests/test_embedding.py`
- **Run all**: `uv run pytest tests/`
- **Coverage**: Check with `pytest --cov`

---

## Common Tasks

### Add a New API Endpoint
1. Create router in `api/routers/feature.py`
2. Create service in `api/feature_service.py`
3. Define schemas in `api/models.py`
4. Register router in `api/main.py`
5. Test via http://localhost:5055/docs

### Add a New LangGraph Workflow
1. Create `open_notebook/graphs/workflow_name.py`
2. Define StateDict and node functions
3. Build graph with `.add_node()` / `.add_edge()`
4. Invoke in service: `graph.ainvoke({"input": ...}, config={"..."})`
5. Test with sample data in `tests/`

### Add Database Migration
1. Create `migrations/XXX_description.surql`
2. Write SurrealQL schema changes
3. Create `migrations/XXX_description_down.surql` (optional rollback)
4. API auto-detects on startup; migration runs if newer than recorded version

### Deploy to Production
1. Review [CONFIGURATION.md](CONFIGURATION.md) for security settings
2. Use `make docker-release` for multi-platform image
3. Push to Docker Hub / GitHub Container Registry
4. Deploy `docker compose --profile multi up`
5. Verify migrations via API logs

---

## Support & Community

- **Documentation**: https://open-notebook.ai
- **Discord**: https://discord.gg/37XJPXfz2w
- **Issues**: https://github.com/lfnovo/open-notebook/issues
- **License**: MIT (see LICENSE)



================================================
FILE: CONFIGURATION.md
================================================
# Configuration Guide

**📍 This file has moved!**

All configuration documentation has been consolidated into the new documentation structure.

👉 **[Read the Configuration Guide](docs/5-CONFIGURATION/index.md)**

---

## Quick Links

- **AI Provider Setup** → [AI Providers](docs/5-CONFIGURATION/ai-providers.md)
- **Environment Variables Reference** → [Environment Reference](docs/5-CONFIGURATION/environment-reference.md)
- **Database Configuration** → [Database Setup](docs/5-CONFIGURATION/database.md)
- **Server Configuration** → [Server Settings](docs/5-CONFIGURATION/server.md)
- **Security Setup** → [Security Configuration](docs/5-CONFIGURATION/security.md)
- **Reverse Proxy** → [Reverse Proxy Setup](docs/5-CONFIGURATION/reverse-proxy.md)
- **Advanced Tuning** → [Advanced Configuration](docs/5-CONFIGURATION/advanced.md)

---

## What You'll Find

The new configuration documentation includes:

- **Complete environment variable reference** with examples
- **Provider-specific setup guides** for OpenAI, Anthropic, Google, Groq, Ollama, and more
- **Production deployment configurations** with security best practices
- **Reverse proxy examples** for Nginx, Caddy, Traefik
- **Database tuning** for performance optimization
- **Troubleshooting guides** for common configuration issues

---

For all configuration details, see **[docs/5-CONFIGURATION/](docs/5-CONFIGURATION/index.md)**.


================================================
FILE: CONTRIBUTING.md
================================================
# Contributing to Open Notebook

**📍 This file has moved!**

All contribution guidelines have been consolidated into the new development documentation structure.

👉 **[Read the Contributing Guide](docs/7-DEVELOPMENT/contributing.md)**

---

## Quick Links

- **Want to contribute code?** → [Contributing Guide](docs/7-DEVELOPMENT/contributing.md)
- **Want to understand the architecture?** → [Architecture Overview](docs/7-DEVELOPMENT/architecture.md)
- **Want to understand our design philosophy?** → [Design Principles](docs/7-DEVELOPMENT/design-principles.md)
- **Are you a maintainer?** → [Maintainer Guide](docs/7-DEVELOPMENT/maintainer-guide.md)
- **New developer?** → [Quick Start](docs/7-DEVELOPMENT/quick-start.md)

---

## The Issue-First Workflow

**TL;DR**: Create an issue first, get it assigned, THEN code.

This prevents wasted effort and ensures your work aligns with the project. [See details →](docs/7-DEVELOPMENT/contributing.md)

---

For all contribution details, see **[docs/7-DEVELOPMENT/contributing.md](docs/7-DEVELOPMENT/contributing.md)**.


================================================
FILE: Dockerfile
================================================
# Build stage
FROM python:3.12-slim-bookworm AS builder

# Install uv using the official method
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/

# Install system dependencies required for building certain Python packages
# Add Node.js 20.x LTS for building frontend
# NOTE: gcc/g++/make removed - uv should download pre-built wheels. Add back if build fails.
# NOTE: gcc/g++/make required for some python dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
    curl \
    build-essential \
    && curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
    && apt-get install -y nodejs \
    && rm -rf /var/lib/apt/lists/*

# Set build optimization environment variables
ENV MAKEFLAGS="-j$(nproc)"
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
ENV UV_COMPILE_BYTECODE=1
ENV UV_LINK_MODE=copy

# Set the working directory in the container to /app
WORKDIR /app

# Copy dependency files and minimal package structure first for better layer caching
COPY pyproject.toml uv.lock ./
COPY open_notebook/__init__.py ./open_notebook/__init__.py

# Install dependencies with optimizations (this layer will be cached unless dependencies change)
RUN uv sync --frozen --no-dev

# Pre-download tiktoken encoding so the app works offline (issue #264).
# /app/tiktoken-cache is intentionally outside /app/data/ so that volume mounts
# of /app/data (for user data persistence) do not hide the pre-baked encoding.
# config.py reads TIKTOKEN_CACHE_DIR from the environment to pick up this path.
ENV TIKTOKEN_CACHE_DIR=/app/tiktoken-cache
RUN mkdir -p /app/tiktoken-cache && \
    .venv/bin/python -c "import tiktoken; tiktoken.get_encoding('o200k_base')"

# Copy the rest of the application code
COPY . /app

# Install frontend dependencies and build
WORKDIR /app/frontend
ARG NPM_REGISTRY=https://registry.npmjs.org/
COPY frontend/package.json frontend/package-lock.json ./
RUN npm config set registry ${NPM_REGISTRY}
RUN npm ci
COPY frontend/ ./
RUN npm run build

# Return to app root
WORKDIR /app

# Runtime stage
FROM python:3.12-slim-bookworm AS runtime

# Install only runtime system dependencies (no build tools)
# Add Node.js 20.x LTS for running frontend
RUN apt-get update && apt-get upgrade -y && apt-get install -y --no-install-recommends \
    ffmpeg \
    supervisor \
    curl \
    && curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
    && apt-get install -y nodejs \
    && rm -rf /var/lib/apt/lists/*

# Install uv using the official method
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/

# Set the working directory in the container to /app
WORKDIR /app

# Copy the virtual environment from builder stage
COPY --from=builder /app/.venv /app/.venv

# Copy the source code (the rest)
COPY . /app

# Copy pre-downloaded tiktoken encoding from builder (outside /data/ — volume-mount safe)
COPY --from=builder /app/tiktoken-cache /app/tiktoken-cache

# Ensure uv uses the existing venv without attempting network operations
ENV UV_NO_SYNC=1
ENV VIRTUAL_ENV=/app/.venv
# Point the app at the pre-baked tiktoken encoding (see open_notebook/config.py)
ENV TIKTOKEN_CACHE_DIR=/app/tiktoken-cache

# Bind Next.js to all interfaces (required for Docker networking and reverse proxies)
ENV HOSTNAME=0.0.0.0

# Copy built frontend from builder stage
COPY --from=builder /app/frontend/.next/standalone /app/frontend/
COPY --from=builder /app/frontend/.next/static /app/frontend/.next/static
COPY --from=builder /app/frontend/public /app/frontend/public
COPY --from=builder /app/frontend/start-server.js /app/frontend/start-server.js

# Expose ports for Frontend and API
EXPOSE 8502 5055

RUN mkdir -p /app/data

# Copy and make executable the wait-for-api script
COPY scripts/wait-for-api.sh /app/scripts/wait-for-api.sh
RUN chmod +x /app/scripts/wait-for-api.sh

# Copy supervisord configuration
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf

# Create log directories
RUN mkdir -p /var/log/supervisor

# Runtime API URL Configuration
# The API_URL environment variable can be set at container runtime to configure
# where the frontend should connect to the API. This allows the same Docker image
# to work in different deployment scenarios without rebuilding.
#
# If not set, the system will auto-detect based on incoming requests.
# Set API_URL when using reverse proxies or custom domains.
#
# Example: docker run -e API_URL=https://your-domain.com/api ...

CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]


================================================
FILE: Dockerfile.single
================================================
# Stage 1: Frontend Builder
FROM node:20-slim AS frontend-builder
WORKDIR /app/frontend

# Copy dependency files first to leverage cache
COPY frontend/package.json frontend/package-lock.json ./
ARG NPM_REGISTRY=https://registry.npmjs.org/
RUN npm config set registry ${NPM_REGISTRY}
RUN npm ci

# Copy the rest of the frontend source
COPY frontend/ ./
# Build the frontend
RUN npm run build

# Stage 2: SurrealDB binary (pinned to v2 to match docker-compose.yml)
FROM surrealdb/surrealdb:v2 AS surreal-binary

# Stage 4: Backend Builder
FROM python:3.12-slim-bookworm AS backend-builder
# Install build dependencies
RUN apt-get update && apt-get upgrade -y && apt-get install -y --no-install-recommends build-essential && rm -rf /var/lib/apt/lists/*
# Install uv
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
WORKDIR /app

# Set build optimization environment variables
ENV UV_HTTP_TIMEOUT=120

# Copy dependency files first
COPY pyproject.toml uv.lock ./
COPY open_notebook/__init__.py ./open_notebook/__init__.py
# Install dependencies
RUN uv sync --frozen --no-dev

# Pre-download tiktoken encoding so the app works offline (issue #264).
# /app/tiktoken-cache is intentionally outside /app/data/ so that volume mounts
# of /app/data (for user data persistence) do not hide the pre-baked encoding.
# config.py reads TIKTOKEN_CACHE_DIR from the environment to pick up this path.
ENV TIKTOKEN_CACHE_DIR=/app/tiktoken-cache
RUN mkdir -p /app/tiktoken-cache && \
    .venv/bin/python -c "import tiktoken; tiktoken.get_encoding('o200k_base')"

# Stage 5: Runtime
FROM python:3.12-slim-bookworm AS runtime

# Install runtime dependencies
RUN apt-get update && apt-get upgrade -y && apt-get install -y \
    ffmpeg \
    supervisor \
    curl \
    && curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
    && apt-get install -y nodejs \
    && rm -rf /var/lib/apt/lists/*

# Install SurrealDB (copied from pinned v2 image to match docker-compose.yml)
COPY --from=surreal-binary /surreal /usr/local/bin/surreal

# Install uv (optional but helpful for some scripts)
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/

WORKDIR /app

# Copy backend virtualenv and source code
COPY --from=backend-builder /app/.venv /app/.venv
COPY . /app/

# Copy pre-downloaded tiktoken encoding from builder (outside /data/ — volume-mount safe)
COPY --from=backend-builder /app/tiktoken-cache /app/tiktoken-cache

# Copy built frontend from standalone output
COPY --from=frontend-builder /app/frontend/.next/standalone /app/frontend/
COPY --from=frontend-builder /app/frontend/.next/static /app/frontend/.next/static
COPY --from=frontend-builder /app/frontend/public /app/frontend/public

# Bind Next.js to all interfaces (required for Docker networking and reverse proxies)
ENV HOSTNAME=0.0.0.0
# Point the app at the pre-baked tiktoken encoding (see open_notebook/config.py)
ENV TIKTOKEN_CACHE_DIR=/app/tiktoken-cache

# Setup directories and permissions
RUN mkdir -p /app/data /mydata

# Ensure wait-for-api script is executable
RUN chmod +x /app/scripts/wait-for-api.sh

# Copy supervisord configuration
COPY supervisord.single.conf /etc/supervisor/conf.d/supervisord.conf

# Create log directories
RUN mkdir -p /var/log/supervisor

# Expose ports
EXPOSE 8502 5055

# Set startup command
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]

================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2024 Luis Novo
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

================================================
FILE: MAINTAINER_GUIDE.md
================================================
# Maintainer Guide

**📍 This file has moved!**

All maintainer guidelines have been consolidated into the new development documentation structure.

👉 **[Read the Maintainer Guide](docs/7-DEVELOPMENT/maintainer-guide.md)**

---

## Quick Links

- **Maintainer Guide** → [docs/7-DEVELOPMENT/maintainer-guide.md](docs/7-DEVELOPMENT/maintainer-guide.md)
- **Contributing Guide** → [docs/7-DEVELOPMENT/contributing.md](docs/7-DEVELOPMENT/contributing.md)
- **Design Principles** → [docs/7-DEVELOPMENT/design-principles.md](docs/7-DEVELOPMENT/design-principles.md)

---

For all maintainer details, see **[docs/7-DEVELOPMENT/maintainer-guide.md](docs/7-DEVELOPMENT/maintainer-guide.md)**.


================================================
FILE: Makefile
================================================
.PHONY: run frontend check ruff database lint api start-all stop-all status clean-cache worker worker-start worker-stop worker-restart
.PHONY: docker-buildx-prepare docker-buildx-clean docker-buildx-reset
.PHONY: docker-push docker-push-latest docker-release docker-build-local tag export-docs

# Get version from pyproject.toml
VERSION := $(shell grep -m1 version pyproject.toml | cut -d'"' -f2)

# Image names for both registries
DOCKERHUB_IMAGE := lfnovo/open_notebook
GHCR_IMAGE := ghcr.io/lfnovo/open-notebook

# Build platforms
PLATFORMS := linux/amd64,linux/arm64

database:
	docker compose up -d surrealdb

run:
	@echo "⚠️  Warning: Starting frontend only. For full functionality, use 'make start-all'"
	cd frontend && npm run dev

frontend:
	cd frontend && npm run dev

lint:
	uv run python -m mypy .

ruff:
	ruff check . --fix

# === Docker Build Setup ===
docker-buildx-prepare:
	@docker buildx inspect multi-platform-builder >/dev/null 2>&1 || \
		docker buildx create --use --name multi-platform-builder --driver docker-container
	@docker buildx use multi-platform-builder

docker-buildx-clean:
	@echo "🧹 Cleaning up buildx builders..."
	@docker buildx rm multi-platform-builder 2>/dev/null || true
	@docker ps -a | grep buildx_buildkit | awk '{print $$1}' | xargs -r docker rm -f 2>/dev/null || true
	@echo "✅ Buildx cleanup complete!"

docker-buildx-reset: docker-buildx-clean docker-buildx-prepare
	@echo "✅ Buildx reset complete!"

# === Docker Build Targets ===

# Build production image for local platform only (no push)
docker-build-local:
	@echo "🔨 Building production image locally ($(shell uname -m))..."
	docker build \
		-t $(DOCKERHUB_IMAGE):$(VERSION) \
		-t $(DOCKERHUB_IMAGE):local \
		.
	@echo "✅ Built $(DOCKERHUB_IMAGE):$(VERSION) and $(DOCKERHUB_IMAGE):local"
	@echo "Run with: docker run -p 5055:5055 -p 3000:3000 $(DOCKERHUB_IMAGE):local"

# Build and push version tags ONLY (no latest) for both regular and single images
docker-push: docker-buildx-prepare
	@echo "📤 Building and pushing version $(VERSION) to both registries..."
	@echo "🔨 Building regular image..."
	docker buildx build --pull \
		--platform $(PLATFORMS) \
		--progress=plain \
		-t $(DOCKERHUB_IMAGE):$(VERSION) \
		-t $(GHCR_IMAGE):$(VERSION) \
		--push \
		.
	@echo "🔨 Building single-container image..."
	docker buildx build --pull \
		--platform $(PLATFORMS) \
		--progress=plain \
		-f Dockerfile.single \
		-t $(DOCKERHUB_IMAGE):$(VERSION)-single \
		-t $(GHCR_IMAGE):$(VERSION)-single \
		--push \
		.
	@echo "✅ Pushed version $(VERSION) to both registries (latest NOT updated)"
	@echo "  📦 Docker Hub:"
	@echo "    - $(DOCKERHUB_IMAGE):$(VERSION)"
	@echo "    - $(DOCKERHUB_IMAGE):$(VERSION)-single"
	@echo "  📦 GHCR:"
	@echo "    - $(GHCR_IMAGE):$(VERSION)"
	@echo "    - $(GHCR_IMAGE):$(VERSION)-single"

# Update v1-latest tags to current version (both regular and single images)
docker-push-latest: docker-buildx-prepare
	@echo "📤 Updating v1-latest tags to version $(VERSION)..."
	@echo "🔨 Building regular image with latest tag..."
	docker buildx build --pull \
		--platform $(PLATFORMS) \
		--progress=plain \
		-t $(DOCKERHUB_IMAGE):$(VERSION) \
		-t $(DOCKERHUB_IMAGE):v1-latest \
		-t $(GHCR_IMAGE):$(VERSION) \
		-t $(GHCR_IMAGE):v1-latest \
		--push \
		.
	@echo "🔨 Building single-container image with latest tag..."
	docker buildx build --pull \
		--platform $(PLATFORMS) \
		--progress=plain \
		-f Dockerfile.single \
		-t $(DOCKERHUB_IMAGE):$(VERSION)-single \
		-t $(DOCKERHUB_IMAGE):v1-latest-single \
		-t $(GHCR_IMAGE):$(VERSION)-single \
		-t $(GHCR_IMAGE):v1-latest-single \
		--push \
		.
	@echo "✅ Updated v1-latest to version $(VERSION)"
	@echo "  📦 Docker Hub:"
	@echo "    - $(DOCKERHUB_IMAGE):$(VERSION) → v1-latest"
	@echo "    - $(DOCKERHUB_IMAGE):$(VERSION)-single → v1-latest-single"
	@echo "  📦 GHCR:"
	@echo "    - $(GHCR_IMAGE):$(VERSION) → v1-latest"
	@echo "    - $(GHCR_IMAGE):$(VERSION)-single → v1-latest-single"

# Full release: push version AND update latest tags
docker-release: docker-push-latest
	@echo "✅ Full release complete for version $(VERSION)"

tag:
	@version=$$(grep '^version = ' pyproject.toml | sed 's/version = "\(.*\)"/\1/'); \
	echo "Creating tag v$$version"; \
	git tag "v$$version"; \
	git push origin "v$$version"


dev:
	docker compose -f docker-compose.dev.yml up --build 

full:
	docker compose -f docker-compose.full.yml up --build 


api:
	uv run --env-file .env run_api.py

.PHONY: worker worker-start worker-stop worker-restart

worker: worker-start

worker-start:
	@echo "Starting surreal-commands worker..."
	uv run --env-file .env surreal-commands-worker --import-modules commands

worker-stop:
	@echo "Stopping surreal-commands worker..."
	pkill -f "surreal-commands-worker" || true

worker-restart: worker-stop
	@sleep 2
	@$(MAKE) worker-start

# === Service Management ===
start-all:
	@echo "🚀 Starting Open Notebook (Database + API + Worker + Frontend)..."
	@echo "📊 Starting SurrealDB..."
	@docker compose -f docker-compose.dev.yml up -d surrealdb
	@sleep 3
	@echo "🔧 Starting API backend..."
	@uv run run_api.py &
	@sleep 3
	@echo "⚙️ Starting background worker..."
	@uv run --env-file .env surreal-commands-worker --import-modules commands &
	@sleep 2
	@echo "🌐 Starting Next.js frontend..."
	@echo "✅ All services started!"
	@echo "📱 Frontend: http://localhost:3000"
	@echo "🔗 API: http://localhost:5055"
	@echo "📚 API Docs: http://localhost:5055/docs"
	cd frontend && npm run dev

stop-all:
	@echo "🛑 Stopping all Open Notebook services..."
	@pkill -f "next dev" || true
	@pkill -f "surreal-commands-worker" || true
	@pkill -f "run_api.py" || true
	@pkill -f "uvicorn api.main:app" || true
	@docker compose down
	@echo "✅ All services stopped!"

status:
	@echo "📊 Open Notebook Service Status:"
	@echo "Database (SurrealDB):"
	@docker compose ps surrealdb 2>/dev/null || echo "  ❌ Not running"
	@echo "API Backend:"
	@pgrep -f "run_api.py\|uvicorn api.main:app" >/dev/null && echo "  ✅ Running" || echo "  ❌ Not running"
	@echo "Background Worker:"
	@pgrep -f "surreal-commands-worker" >/dev/null && echo "  ✅ Running" || echo "  ❌ Not running"
	@echo "Next.js Frontend:"
	@pgrep -f "next dev" >/dev/null && echo "  ✅ Running" || echo "  ❌ Not running"

# === Documentation Export ===
export-docs:
	@echo "📚 Exporting documentation..."
	@uv run python scripts/export_docs.py
	@echo "✅ Documentation export complete!"

# === Cleanup ===
clean-cache:
	@echo "🧹 Cleaning cache directories..."
	@find . -name "__pycache__" -type d -exec rm -rf {} + 2>/dev/null || true
	@find . -name ".mypy_cache" -type d -exec rm -rf {} + 2>/dev/null || true
	@find . -name ".ruff_cache" -type d -exec rm -rf {} + 2>/dev/null || true
	@find . -name ".pytest_cache" -type d -exec rm -rf {} + 2>/dev/null || true
	@find . -name "*.pyc" -type f -delete 2>/dev/null || true
	@find . -name "*.pyo" -type f -delete 2>/dev/null || true
	@find . -name "*.pyd" -type f -delete 2>/dev/null || true
	@echo "✅ Cache directories cleaned!"

================================================
FILE: README.dev.md
================================================
# Developer Guide

This guide is for developers working on Open Notebook. For end-user documentation, see [README.md](README.md) and [docs/](docs/).

## Quick Start for Development

```bash
# 1. Clone and setup
git clone https://github.com/lfnovo/open-notebook.git
cd open-notebook

# 2. Copy environment files
cp .env.example .env
cp .env.example docker.env

# 3. Install dependencies
uv sync

# 4. Start all services (recommended for development)
make start-all
```

## Development Workflows

### When to Use What?

| Workflow | Use Case | Speed | Production Parity |
|----------|----------|-------|-------------------|
| **Local Services** (`make start-all`) | Day-to-day development, fastest iteration | ⚡⚡⚡ Fast | Medium |
| **Docker Compose** (`make dev`) | Testing containerized setup | ⚡⚡ Medium | High |
| **Local Docker Build** (`make docker-build-local`) | Testing Dockerfile changes | ⚡ Slow | Very High |
| **Multi-platform Build** (`make docker-push`) | Publishing releases | 🐌 Very Slow | Exact |

---

## 1. Local Development (Recommended)

**Best for:** Daily development, hot reload, debugging

### Setup

```bash
# Start database
make database

# Start all services (DB + API + Worker + Frontend)
make start-all
```

### What This Does

1. Starts SurrealDB in Docker (port 8000)
2. Starts FastAPI backend (port 5055)
3. Starts background worker (surreal-commands)
4. Starts Next.js frontend (port 3000)

### Individual Services

```bash
# Just the database
make database

# Just the API
make api

# Just the frontend
make frontend

# Just the worker
make worker
```

### Checking Status

```bash
# See what's running
make status

# Stop everything
make stop-all
```

### Advantages
- ✅ Fastest iteration (hot reload)
- ✅ Easy debugging (direct process access)
- ✅ Low resource usage
- ✅ Direct log access

### Disadvantages
- ❌ Doesn't test Docker build
- ❌ Environment may differ from production
- ❌ Requires local Python/Node setup

---

## 2. Docker Compose Development

**Best for:** Testing containerized setup, CI/CD verification

```bash
# Start with dev profile
make dev

# Or full stack
make full
```

### Configuration Files

- `docker-compose.dev.yml` - Development setup
- `docker-compose.full.yml` - Full stack setup
- `docker-compose.yml` - Base configuration

### Advantages
- ✅ Closer to production environment
- ✅ Isolated dependencies
- ✅ Easy to share exact environment

### Disadvantages
- ❌ Slower rebuilds
- ❌ More complex debugging
- ❌ Higher resource usage

---

## 3. Testing Production Docker Images

**Best for:** Verifying Dockerfile changes before publishing

### Build Locally

```bash
# Build production image for your platform only
make docker-build-local
```

This creates two tags:
- `lfnovo/open_notebook:<version>` (from pyproject.toml)
- `lfnovo/open_notebook:local`

### Run Locally

```bash
docker run -p 5055:5055 -p 3000:3000 lfnovo/open_notebook:local
```

### When to Use
- ✅ Before pushing to registry
- ✅ Testing Dockerfile changes
- ✅ Debugging production-specific issues
- ✅ Verifying build process

---

## 4. Publishing Docker Images

### Workflow

```bash
# 1. Test locally first
make docker-build-local

# 2. If successful, push version tag (no latest update)
make docker-push

# 3. Test the pushed version in staging/production

# 4. When ready, promote to latest
make docker-push-latest
```

### Available Commands

| Command | What It Does | Updates Latest? |
|---------|--------------|-----------------|
| `make docker-build-local` | Build for current platform only | No registry push |
| `make docker-push` | Push version tags to registries | ❌ No |
| `make docker-push-latest` | Push version + update v1-latest | ✅ Yes |
| `make docker-release` | Full release (same as docker-push-latest) | ✅ Yes |

### Publishing Details

- **Platforms:** `linux/amd64`, `linux/arm64`
- **Registries:** Docker Hub + GitHub Container Registry
- **Image Variants:** Regular + Single-container (`-single`)
- **Version Source:** `pyproject.toml`

### Creating Git Tags

```bash
# Create and push git tag matching pyproject.toml version
make tag
```

---

## Code Quality

```bash
# Run linter with auto-fix
make ruff

# Run type checking
make lint

# Run tests
uv run pytest tests/

# Clean cache directories
make clean-cache
```

---

## Common Development Tasks

### Adding a New Feature

1. Create feature branch
2. Develop using `make start-all`
3. Write tests
4. Run `make ruff` and `make lint`
5. Test with `make docker-build-local`
6. Create PR

### Fixing a Bug

1. Reproduce locally with `make start-all`
2. Add test case demonstrating bug
3. Fix the bug
4. Verify test passes
5. Check with `make docker-build-local`

### Updating Dependencies

```bash
# Add Python dependency
uv add package-name

# Update dependencies
uv sync

# Frontend dependencies
cd frontend && npm install package-name
```

### Adding a New Language (i18n)

Open Notebook supports internationalization. To add a new language:

1. **Create locale file**: Copy an existing locale as template
   ```bash
   cp frontend/src/lib/locales/en-US/index.ts frontend/src/lib/locales/pt-BR/index.ts
   ```

2. **Translate all strings** in the new file. The structure includes:
   - `common`: Shared UI elements (buttons, labels)
   - `notebooks`, `sources`, `notes`: Feature-specific strings
   - `chat`, `search`, `podcasts`: Module-specific strings
   - `apiErrors`: Error message translations

3. **Register the locale** in `frontend/src/lib/locales/index.ts`:
   ```typescript
   import { ptBR } from './pt-BR'

   export const locales = {
     'en-US': enUS,
     'zh-CN': zhCN,
     'zh-TW': zhTW,
     'pt-BR': ptBR,  // Add your locale
   }
   ```

4. **Add date-fns locale** in `frontend/src/lib/utils/date-locale.ts`:
   ```typescript
   import { zhCN, enUS, zhTW, ptBR } from 'date-fns/locale'

   const LOCALE_MAP: Record<string, Locale> = {
     'zh-CN': zhCN,
     'zh-TW': zhTW,
     'en-US': enUS,
     'pt-BR': ptBR,  // Add your locale
   }
   ```

5. **Test**: Switch languages using the language toggle in the UI header.

### Database Migrations

Database migrations run **automatically** when the API starts.

1. Create migration file: `migrations/XXX_description.surql`
2. Write SurrealQL schema changes
3. (Optional) Create rollback: `migrations/XXX_description_down.surql`
4. Restart API - migration runs on startup

---

## Troubleshooting

### Services Won't Start

```bash
# Check status
make status

# Check database
docker compose ps surrealdb

# View logs
docker compose logs surrealdb

# Restart everything
make stop-all
make start-all
```

### Port Already in Use

```bash
# Find process using port
lsof -i :5055
lsof -i :3000
lsof -i :8000

# Kill stuck processes
make stop-all
```

### Database Connection Issues

```bash
# Verify SurrealDB is running
docker compose ps surrealdb

# Check connection settings in .env
cat .env | grep SURREAL
```

### Docker Build Fails

```bash
# Clean Docker cache
docker builder prune

# Reset buildx
make docker-buildx-reset

# Try local build first
make docker-build-local
```

---

## Project Structure

```
open-notebook/
├── api/                    # FastAPI backend
├── frontend/               # Next.js React frontend
├── open_notebook/          # Python core library
│   ├── domain/            # Domain models
│   ├── graphs/            # LangGraph workflows
│   ├── ai/                # AI provider integration
│   └── database/          # SurrealDB operations
├── migrations/             # Database migrations
├── tests/                  # Test suite
├── docs/                   # User documentation
└── Makefile               # Development commands
```

See component-specific CLAUDE.md files for detailed architecture:
- [frontend/CLAUDE.md](frontend/CLAUDE.md)
- [api/CLAUDE.md](api/CLAUDE.md)
- [open_notebook/CLAUDE.md](open_notebook/CLAUDE.md)

---

## Environment Variables

### Required for Local Development

```bash
# .env file
SURREAL_URL=ws://localhost:8000
SURREAL_USER=root
SURREAL_PASS=root
SURREAL_DB=open_notebook
SURREAL_NS=production

# AI Provider (at least one required)
OPENAI_API_KEY=sk-...
# OR
ANTHROPIC_API_KEY=sk-ant-...
# OR configure other providers (see docs/5-CONFIGURATION/)
```

See [docs/5-CONFIGURATION/](docs/5-CONFIGURATION/) for complete configuration guide.

---

## Performance Tips

### Speed Up Local Development

1. **Use `make start-all`** instead of Docker for daily work
2. **Keep SurrealDB running** between sessions (`make database`)
3. **Use `make docker-build-local`** only when testing Dockerfile changes
4. **Skip multi-platform builds** until ready to publish

### Reduce Resource Usage

```bash
# Stop unused services
make stop-all

# Clean up Docker
docker system prune -a

# Clean Python cache
make clean-cache
```

---

## TODO: Sections to Add

- [ ] Frontend development guide (hot reload, component structure)
- [ ] API development guide (adding endpoints, services)
- [ ] LangGraph workflow development
- [ ] Testing strategy and coverage
- [ ] Debugging tips (VSCode/PyCharm setup)
- [ ] CI/CD pipeline overview
- [ ] Release process checklist
- [ ] Common error messages and solutions

---

## Resources

- **Documentation:** https://open-notebook.ai
- **Discord:** https://discord.gg/37XJPXfz2w
- **Issues:** https://github.com/lfnovo/open-notebook/issues
- **Contributing:** [CONTRIBUTING.md](CONTRIBUTING.md)
- **Maintainer Guide:** [MAINTAINER_GUIDE.md](MAINTAINER_GUIDE.md)

---

**Last Updated:** January 2025


================================================
FILE: README.md
================================================
<a id="readme-top"></a>

<!-- [![Contributors][contributors-shield]][contributors-url] -->
[![Forks][forks-shield]][forks-url]
[![Stargazers][stars-shield]][stars-url]
[![Issues][issues-shield]][issues-url]
[![MIT License][license-shield]][license-url]
<!-- [![LinkedIn][linkedin-shield]][linkedin-url] -->


<!-- PROJECT LOGO -->
<br />
<div align="center">
  <a href="https://github.com/lfnovo/open-notebook">
    <img src="docs/assets/hero.svg" alt="Logo">
  </a>

  <h3 align="center">Open Notebook</h3>

  <p align="center">
    An open source, privacy-focused alternative to Google's Notebook LM!
    <br /><strong>Join our <a href="https://discord.gg/37XJPXfz2w">Discord server</a> for help, to share workflow ideas, and suggest features!</strong>
    <br />
    <a href="https://www.open-notebook.ai"><strong>Checkout our website »</strong></a>
    <br />
    <br />
    <a href="docs/0-START-HERE/index.md">📚 Get Started</a>
    ·
    <a href="docs/3-USER-GUIDE/index.md">📖 User Guide</a>
    ·
    <a href="docs/2-CORE-CONCEPTS/index.md">✨ Features</a>
    ·
    <a href="docs/1-INSTALLATION/index.md">🚀 Deploy</a>
  </p>
</div>

<p align="center">
<a href="https://trendshift.io/repositories/14536" target="_blank"><img src="https://trendshift.io/api/badge/repositories/14536" alt="lfnovo%2Fopen-notebook | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
</p>

<div align="center">
  <!-- Keep these links. Translations will automatically update with the README. -->
  <a href="https://zdoc.app/de/lfnovo/open-notebook">Deutsch</a> | 
  <a href="https://zdoc.app/es/lfnovo/open-notebook">Español</a> | 
  <a href="https://zdoc.app/fr/lfnovo/open-notebook">français</a> | 
  <a href="https://zdoc.app/ja/lfnovo/open-notebook">日本語</a> | 
  <a href="https://zdoc.app/ko/lfnovo/open-notebook">한국어</a> | 
  <a href="https://zdoc.app/pt/lfnovo/open-notebook">Português</a> | 
  <a href="https://zdoc.app/ru/lfnovo/open-notebook">Русский</a> | 
  <a href="https://zdoc.app/zh/lfnovo/open-notebook">中文</a>
</div>

## A private, multi-model, 100% local, full-featured alternative to Notebook LM

![New Notebook](docs/assets/asset_list.png)

In a world dominated by Artificial Intelligence, having the ability to think 🧠 and acquire new knowledge 💡, is a skill that should not be a privilege for a few, nor restricted to a single provider.

**Open Notebook empowers you to:**
- 🔒 **Control your data** - Keep your research private and secure
- 🤖 **Choose your AI models** - Support for 16+ providers including OpenAI, Anthropic, Ollama, LM Studio, and more
- 📚 **Organize multi-modal content** - PDFs, videos, audio, web pages, and more
- 🎙️ **Generate professional podcasts** - Advanced multi-speaker podcast generation
- 🔍 **Search intelligently** - Full-text and vector search across all your content
- 💬 **Chat with context** - AI conversations powered by your research
- 🌐 **Multi-language UI** - English, Portuguese, Chinese (Simplified & Traditional), Japanese, Russian, and Bengali support

Learn more about our project at [https://www.open-notebook.ai](https://www.open-notebook.ai)

---

## 🆚 Open Notebook vs Google Notebook LM

| Feature | Open Notebook | Google Notebook LM | Advantage |
|---------|---------------|--------------------|-----------|
| **Privacy & Control** | Self-hosted, your data | Google cloud only | Complete data sovereignty |
| **AI Provider Choice** | 16+ providers (OpenAI, Anthropic, Ollama, LM Studio, etc.) | Google models only | Flexibility and cost optimization |
| **Podcast Speakers** | 1-4 speakers with custom profiles | 2 speakers only | Extreme flexibility |
| **Content Transformations** | Custom and built-in | Limited options | Unlimited processing power |
| **API Access** | Full REST API | No API | Complete automation |
| **Deployment** | Docker, cloud, or local | Google hosted only | Deploy anywhere |
| **Citations** | Basic references (will improve) | Comprehensive with sources | Research integrity |
| **Customization** | Open source, fully customizable | Closed system | Unlimited extensibility |
| **Cost** | Pay only for AI usage | Free tier + Monthly subscription | Transparent and controllable |

**Why Choose Open Notebook?**
- 🔒 **Privacy First**: Your sensitive research stays completely private
- 💰 **Cost Control**: Choose cheaper AI providers or run locally with Ollama
- 🎙️ **Better Podcasts**: Full script control and multi-speaker flexibility vs limited 2-speaker deep-dive format
- 🔧 **Unlimited Customization**: Modify, extend, and integrate as needed
- 🌐 **No Vendor Lock-in**: Switch providers, deploy anywhere, own your data

### Built With

[![Python][Python]][Python-url] [![Next.js][Next.js]][Next-url] [![React][React]][React-url] [![SurrealDB][SurrealDB]][SurrealDB-url] [![LangChain][LangChain]][LangChain-url]

## 🚀 Quick Start (2 Minutes)

### Prerequisites
- [Docker Desktop](https://www.docker.com/products/docker-desktop/) installed
- That's it! (API keys configured later in the UI)

### Step 1: Get docker-compose.yml

**Option A:** Download directly
```bash
curl -o docker-compose.yml https://raw.githubusercontent.com/lfnovo/open-notebook/main/docker-compose.yml
```

**Option B:** Create the file manually
Copy this into a new file called `docker-compose.yml`:

```yaml
services:
  surrealdb:
    image: surrealdb/surrealdb:v2
    command: start --log info --user root --pass root rocksdb:/mydata/mydatabase.db
    user: root
    ports:
      - "8000:8000"
    volumes:
      - ./surreal_data:/mydata
    restart: always

  open_notebook:
    image: lfnovo/open_notebook:v1-latest
    ports:
      - "8502:8502"
      - "5055:5055"
    environment:
      - OPEN_NOTEBOOK_ENCRYPTION_KEY=change-me-to-a-secret-string
      - SURREAL_URL=ws://surrealdb:8000/rpc
      - SURREAL_USER=root
      - SURREAL_PASSWORD=root
      - SURREAL_NAMESPACE=open_notebook
      - SURREAL_DATABASE=open_notebook
    volumes:
      - ./notebook_data:/app/data
    depends_on:
      - surrealdb
    restart: always
```

### Step 2: Set Your Encryption Key
Edit `docker-compose.yml` and change this line:
```yaml
- OPEN_NOTEBOOK_ENCRYPTION_KEY=change-me-to-a-secret-string
```
to any secret value (e.g., `my-super-secret-key-123`)

### Step 3: Start Services
```bash
docker compose up -d
```

Wait 15-20 seconds, then open: **http://localhost:8502**

### Step 4: Configure AI Provider
1. Go to **Settings** → **API Keys**
2. Click **Add Credential**
3. Choose your provider (OpenAI, Anthropic, Google, etc.)
4. Paste your API key and click **Save**
5. Click **Test Connection** → **Discover Models** → **Register Models**

Done! You're ready to create your first notebook.

> **Need an API key?** Get one from:
> [OpenAI](https://platform.openai.com/api-keys) · [Anthropic](https://console.anthropic.com/) · [Google](https://aistudio.google.com/) · [Groq](https://console.groq.com/) (free tier)

> **Want free local AI?** See [examples/docker-compose-ollama.yml](examples/) for Ollama setup

---

### 📚 More Installation Options

- **[With Ollama (Free Local AI)](examples/docker-compose-ollama.yml)** - Run models locally without API costs
- **[From Source (Developers)](docs/1-INSTALLATION/from-source.md)** - For development and contributions
- **[Complete Installation Guide](docs/1-INSTALLATION/index.md)** - All deployment scenarios

---

### 📖 Need Help?

- **🤖 AI Installation Assistant**: [CustomGPT to help you install](https://chatgpt.com/g/g-68776e2765b48191bd1bae3f30212631-open-notebook-installation-assistant)
- **🆘 Troubleshooting**: [5-minute troubleshooting guide](docs/6-TROUBLESHOOTING/quick-fixes.md)
- **💬 Community Support**: [Discord Server](https://discord.gg/37XJPXfz2w)
- **🐛 Report Issues**: [GitHub Issues](https://github.com/lfnovo/open-notebook/issues)

---

## Star History

[![Star History Chart](https://api.star-history.com/svg?repos=lfnovo/open-notebook&type=date&legend=top-left)](https://www.star-history.com/#lfnovo/open-notebook&type=date&legend=top-left)


## Provider Support Matrix

Thanks to the [Esperanto](https://github.com/lfnovo/esperanto) library, we support this providers out of the box!

| Provider     | LLM Support | Embedding Support | Speech-to-Text | Text-to-Speech |
|--------------|-------------|------------------|----------------|----------------|
| OpenAI       | ✅          | ✅               | ✅             | ✅             |
| Anthropic    | ✅          | ❌               | ❌             | ❌             |
| Groq         | ✅          | ❌               | ✅             | ❌             |
| Google (GenAI) | ✅          | ✅               | ❌             | ✅             |
| Vertex AI    | ✅          | ✅               | ❌             | ✅             |
| Ollama       | ✅          | ✅               | ❌             | ❌             |
| Perplexity   | ✅          | ❌               | ❌             | ❌             |
| ElevenLabs   | ❌          | ❌               | ✅             | ✅             |
| Azure OpenAI | ✅          | ✅               | ❌             | ❌             |
| Mistral      | ✅          | ✅               | ❌             | ❌             |
| DeepSeek     | ✅          | ❌               | ❌             | ❌             |
| Voyage       | ❌          | ✅               | ❌             | ❌             |
| xAI          | ✅          | ❌               | ❌             | ❌             |
| OpenRouter   | ✅          | ❌               | ❌             | ❌             |
| OpenAI Compatible* | ✅          | ❌               | ❌             | ❌             |

*Supports LM Studio and any OpenAI-compatible endpoint

## ✨ Key Features

### Core Capabilities
- **🔒 Privacy-First**: Your data stays under your control - no cloud dependencies
- **🎯 Multi-Notebook Organization**: Manage multiple research projects seamlessly
- **📚 Universal Content Support**: PDFs, videos, audio, web pages, Office docs, and more
- **🤖 Multi-Model AI Support**: 16+ providers including OpenAI, Anthropic, Ollama, Google, LM Studio, and more
- **🎙️ Professional Podcast Generation**: Advanced multi-speaker podcasts with Episode Profiles
- **🔍 Intelligent Search**: Full-text and vector search across all your content
- **💬 Context-Aware Chat**: AI conversations powered by your research materials
- **📝 AI-Assisted Notes**: Generate insights or write notes manually

### Advanced Features
- **⚡ Reasoning Model Support**: Full support for thinking models like DeepSeek-R1 and Qwen3
- **🔧 Content Transformations**: Powerful customizable actions to summarize and extract insights
- **🌐 Comprehensive REST API**: Full programmatic access for custom integrations [![API Docs](https://img.shields.io/badge/API-Documentation-blue?style=flat-square)](http://localhost:5055/docs)
- **🔐 Optional Password Protection**: Secure public deployments with authentication
- **📊 Fine-Grained Context Control**: Choose exactly what to share with AI models
- **📎 Citations**: Get answers with proper source citations


## Podcast Feature

[![Check out our podcast sample](https://img.youtube.com/vi/D-760MlGwaI/0.jpg)](https://www.youtube.com/watch?v=D-760MlGwaI)

## 📚 Documentation

### Getting Started
- **[📖 Introduction](docs/0-START-HERE/index.md)** - Learn what Open Notebook offers
- **[⚡ Quick Start](docs/0-START-HERE/quick-start.md)** - Get up and running in 5 minutes
- **[🔧 Installation](docs/1-INSTALLATION/index.md)** - Comprehensive setup guide
- **[🎯 Your First Notebook](docs/0-START-HERE/first-notebook.md)** - Step-by-step tutorial

### User Guide
- **[📱 Interface Overview](docs/3-USER-GUIDE/interface-overview.md)** - Understanding the layout
- **[📚 Notebooks](docs/3-USER-GUIDE/notebooks.md)** - Organizing your research
- **[📄 Sources](docs/3-USER-GUIDE/sources.md)** - Managing content types
- **[📝 Notes](docs/3-USER-GUIDE/notes.md)** - Creating and managing notes
- **[💬 Chat](docs/3-USER-GUIDE/chat.md)** - AI conversations
- **[🔍 Search](docs/3-USER-GUIDE/search.md)** - Finding information

### Advanced Topics
- **[🎙️ Podcast Generation](docs/2-CORE-CONCEPTS/podcasts.md)** - Create professional podcasts
- **[🔧 Content Transformations](docs/2-CORE-CONCEPTS/transformations.md)** - Customize content processing
- **[🤖 AI Models](docs/4-AI-PROVIDERS/index.md)** - AI model configuration
- **[🔌 MCP Integration](docs/5-CONFIGURATION/mcp-integration.md)** - Connect with Claude Desktop, VS Code and other MCP clients
- **[🔧 REST API Reference](docs/7-DEVELOPMENT/api-reference.md)** - Complete API documentation
- **[🔐 Security](docs/5-CONFIGURATION/security.md)** - Password protection and privacy
- **[🚀 Deployment](docs/1-INSTALLATION/index.md)** - Complete deployment guides for all scenarios

<p align="right">(<a href="#readme-top">back to top</a>)</p>

## 🗺️ Roadmap

### Upcoming Features
- **Live Front-End Updates**: Real-time UI updates for smoother experience
- **Async Processing**: Faster UI through asynchronous content processing
- **Cross-Notebook Sources**: Reuse research materials across projects
- **Bookmark Integration**: Connect with your favorite bookmarking apps

### Recently Completed ✅
- **Next.js Frontend**: Modern React-based frontend with improved performance
- **Comprehensive REST API**: Full programmatic access to all functionality
- **Multi-Model Support**: 16+ AI providers including OpenAI, Anthropic, Ollama, LM Studio
- **Advanced Podcast Generator**: Professional multi-speaker podcasts with Episode Profiles
- **Content Transformations**: Powerful customizable actions for content processing
- **Enhanced Citations**: Improved layout and finer control for source citations
- **Multiple Chat Sessions**: Manage different conversations within notebooks

See the [open issues](https://github.com/lfnovo/open-notebook/issues) for a full list of proposed features and known issues.

<p align="right">(<a href="#readme-top">back to top</a>)</p>


## 📖 Need Help?
- **🤖 AI Installation Assistant**: We have a [CustomGPT built to help you install Open Notebook](https://chatgpt.com/g/g-68776e2765b48191bd1bae3f30212631-open-notebook-installation-assistant) - it will guide you through each step!
- **New to Open Notebook?** Start with our [Getting Started Guide](docs/0-START-HERE/index.md)
- **Need installation help?** Check our [Installation Guide](docs/1-INSTALLATION/index.md)
- **Want to see it in action?** Try our [Quick Start Tutorial](docs/0-START-HERE/quick-start.md)

## 🤝 Community & Contributing

### Join the Community
- 💬 **[Discord Server](https://discord.gg/37XJPXfz2w)** - Get help, share ideas, and connect with other users
- 🐛 **[GitHub Issues](https://github.com/lfnovo/open-notebook/issues)** - Report bugs and request features
- ⭐ **Star this repo** - Show your support and help others discover Open Notebook

### Contributing
We welcome contributions! We're especially looking for help with:
- **Frontend Development**: Help improve our modern Next.js/React UI
- **Testing & Bug Fixes**: Make Open Notebook more robust
- **Feature Development**: Build the coolest research tool together
- **Documentation**: Improve guides and tutorials

**Current Tech Stack**: Python, FastAPI, Next.js, React, SurrealDB
**Future Roadmap**: Real-time updates, enhanced async processing

See our [Contributing Guide](CONTRIBUTING.md) for detailed information on how to get started.

<p align="right">(<a href="#readme-top">back to top</a>)</p>


## 📄 License

Open Notebook is MIT licensed. See the [LICENSE](LICENSE) file for details.


**Community Support**:
- 💬 [Discord Server](https://discord.gg/37XJPXfz2w) - Get help, share ideas, and connect with users
- 🐛 [GitHub Issues](https://github.com/lfnovo/open-notebook/issues) - Report bugs and request features
- 🌐 [Website](https://www.open-notebook.ai) - Learn more about the project

<p align="right">(<a href="#readme-top">back to top</a>)</p>


<!-- MARKDOWN LINKS & IMAGES -->
<!-- https://www.markdownguide.org/basic-syntax/#reference-style-links -->
[contributors-shield]: https://img.shields.io/github/contributors/lfnovo/open-notebook.svg?style=for-the-badge
[contributors-url]: https://github.com/lfnovo/open-notebook/graphs/contributors
[forks-shield]: https://img.shields.io/github/forks/lfnovo/open-notebook.svg?style=for-the-badge
[forks-url]: https://github.com/lfnovo/open-notebook/network/members
[stars-shield]: https://img.shields.io/github/stars/lfnovo/open-notebook.svg?style=for-the-badge
[stars-url]: https://github.com/lfnovo/open-notebook/stargazers
[issues-shield]: https://img.shields.io/github/issues/lfnovo/open-notebook.svg?style=for-the-badge
[issues-url]: https://github.com/lfnovo/open-notebook/issues
[license-shield]: https://img.shields.io/github/license/lfnovo/open-notebook.svg?style=for-the-badge
[license-url]: https://github.com/lfnovo/open-notebook/blob/master/LICENSE.txt
[linkedin-shield]: https://img.shields.io/badge/-LinkedIn-black.svg?style=for-the-badge&logo=linkedin&colorB=555
[linkedin-url]: https://linkedin.com/in/lfnovo
[product-screenshot]: images/screenshot.png
[Next.js]: https://img.shields.io/badge/Next.js-000000?style=for-the-badge&logo=next.js&logoColor=white
[Next-url]: https://nextjs.org/
[React]: https://img.shields.io/badge/React-61DAFB?style=for-the-badge&logo=react&logoColor=black
[React-url]: https://reactjs.org/
[Python]: https://img.shields.io/badge/Python-3776AB?style=for-the-badge&logo=python&logoColor=white
[Python-url]: https://www.python.org/
[LangChain]: https://img.shields.io/badge/LangChain-3A3A3A?style=for-the-badge&logo=chainlink&logoColor=white
[LangChain-url]: https://www.langchain.com/
[SurrealDB]: https://img.shields.io/badge/SurrealDB-FF5E00?style=for-the-badge&logo=databricks&logoColor=white
[SurrealDB-url]: https://surrealdb.com/


================================================
FILE: api/CLAUDE.md
================================================
# API Module

FastAPI-based REST backend exposing services for notebooks, sources, notes, chat, podcasts, and AI model management.

## Purpose

FastAPI application serving three architectural layers: routes (HTTP endpoints), services (business logic), and models (request/response schemas). Integrates LangGraph workflows (chat, ask, source_chat), SurrealDB persistence, and AI providers via Esperanto.

## Architecture Overview

**Three layers**:
1. **Routes** (`routers/*`): HTTP endpoints mapping to services
2. **Services** (`*_service.py`): Business logic orchestrating domain models, database, graphs, AI providers
3. **Models** (`models.py`): Pydantic request/response schemas with validation

**Startup flow**:
- Load .env environment variables
- Initialize CORS middleware + password auth middleware
- Run database migrations via AsyncMigrationManager on lifespan startup
- Run podcast profile data migration (legacy string to model registry conversion)
- Register all routers

**Key services**:
- `chat_service.py`: Invokes chat graph with messages, context
- `podcast_service.py`: Orchestrates outline + transcript generation
- `sources_service.py`: Content ingestion, vectorization, metadata
- `notes_service.py`: Note creation, linking to sources/insights
- `transformations_service.py`: Applies transformations to content
- `models_service.py`: Manages AI provider/model configuration
- `episode_profiles_service.py`: Manages podcast speaker/episode profiles

## Component Catalog

### Main Application
- **main.py**: FastAPI app initialization, CORS setup, auth middleware, lifespan event, router registration
- **Lifespan handler**: Runs AsyncMigrationManager on startup (database schema migration)
- **Auth middleware**: PasswordAuthMiddleware protects endpoints (password-based access control)

### Services (Business Logic)
- **chat_service.py**: Invokes chat.py graph; handles message history via SqliteSaver
- **podcast_service.py**: Generates outline (outline.jinja), then transcript (transcript.jinja) for episodes
- **sources_service.py**: Ingests files/URLs (content_core), extracts text, vectorizes, saves to SurrealDB
- **transformations_service.py**: Applies transformations via transformation.py graph
- **models_service.py**: Manages ModelManager config (AI provider overrides)
- **episode_profiles_service.py**: CRUD for EpisodeProfile and SpeakerProfile models
- **insights_service.py**: Generates and retrieves source insights
- **notes_service.py**: Creates notes linked to sources/insights

### Models (Schemas)
- **models.py**: Pydantic schemas for request/response validation
- Request bodies: ChatRequest, CreateNoteRequest, PodcastGenerationRequest, etc.
- Response bodies: ChatResponse, NoteResponse, PodcastResponse, etc.
- Custom validators for enum fields, file paths, model references

### Routers
- **routers/chat.py**: POST /chat
- **routers/source_chat.py**: POST /source/{source_id}/chat
- **routers/podcasts.py**: POST /podcasts, GET /podcasts/{id}, POST /podcasts/episodes/{id}/retry, etc.
- **routers/notes.py**: POST /notes, GET /notes/{id}
- **routers/sources.py**: POST /sources, GET /sources/{id}, DELETE /sources/{id}
- **routers/models.py**: GET /models, POST /models/config
- **routers/credentials.py**: CRUD + test + discover + migrate for credential management
- **routers/transformations.py**: POST /transformations
- **routers/insights.py**: GET /sources/{source_id}/insights
- **routers/auth.py**: POST /auth/password (password-based auth)
- **routers/languages.py**: GET /languages (available podcast languages via pycountry+babel)
- **routers/commands.py**: GET /commands/{command_id} (job status tracking)

## Common Patterns

- **Service injection via FastAPI**: Routers import services directly; no DI framework
- **Async/await throughout**: All DB queries, graph invocations, AI calls are async
- **SurrealDB transactions**: Services use repo_query, repo_create, repo_upsert from database layer
- **Config override pattern**: Models/config override via models_service passed to graph.ainvoke(config=...)
- **Error handling**: Custom exception hierarchy (`open_notebook.exceptions`) with global FastAPI exception handlers mapping to HTTP status codes (see Error Handling section below). LangGraph nodes use `classify_error()` to convert raw LLM provider errors into typed exceptions with user-friendly messages.
- **Logging**: loguru logger in main.py; services expected to log key operations
- **Response normalization**: All responses follow standard schema (data + metadata structure)

## Key Dependencies

- `fastapi`: FastAPI app, routers, HTTPException
- `pydantic`: Validation models with Field, field_validator
- `open_notebook.graphs`: chat, ask, source_chat, source, transformation graphs
- `open_notebook.database`: SurrealDB repository functions (repo_query, repo_create, repo_upsert)
- `open_notebook.domain`: Notebook, Source, Note, SourceInsight models
- `open_notebook.ai.provision`: provision_langchain_model() factory
- `ai_prompter`: Prompter for template rendering
- `content_core`: extract_content() for file/URL processing
- `esperanto`: AI provider client library (LLM, embeddings, TTS)
- `surreal_commands`: Job queue for async operations (podcast generation)
- `loguru`: Structured logging

## Important Quirks & Gotchas

- **Migration auto-run**: Database schema migrations run on every API startup (via lifespan); no manual migration steps
- **PasswordAuthMiddleware is basic**: Uses simple password check; production deployments should replace with OAuth/JWT
- **No request rate limiting**: No built-in rate limiting; deployment must add via proxy/middleware
- **Service state is stateless**: Services don't cache results; each request re-queries database/AI models
- **Graph invocation is blocking**: chat/podcast workflows may take minutes; no timeout handling in services
- **Command job fire-and-forget**: podcast_service.py submits jobs but doesn't wait (async job queue pattern)
- **Model override scoping**: Model config override via RunnableConfig is per-request only (not persistent)
- **CORS open by default**: main.py CORS settings allow all origins (restrict before production)
- **No OpenAPI security scheme**: API docs available without auth (disable before production)
- **Services don't validate user permission**: All endpoints trust authentication layer; no per-notebook permission checks

## Error Handling

### Global Exception Handlers (`main.py`)

FastAPI exception handlers map custom exception types from `open_notebook.exceptions` to HTTP status codes. All error responses include CORS headers.

| Exception Class | HTTP Status | Use Case |
|----------------|-------------|----------|
| `NotFoundError` | 404 | Resource not found |
| `InvalidInputError` | 400 | Bad request data |
| `AuthenticationError` | 401 | Invalid/missing API key |
| `RateLimitError` | 429 | Provider rate limit exceeded |
| `ConfigurationError` | 422 | Wrong model name, missing config |
| `NetworkError` | 502 | Cannot reach AI provider |
| `ExternalServiceError` | 502 | Provider returned error (500/503, context length) |
| `OpenNotebookError` (base) | 500 | Any other application error |

### Error Classification (`open_notebook.utils.error_classifier`)

The `classify_error()` function maps raw exceptions from LLM providers/Esperanto/LangChain into the typed exceptions above with user-friendly messages. Used in all LangGraph graph nodes and SSE streaming handlers.

**Flow**: Raw exception → keyword matching → `(ExceptionClass, user_message)` → raised → caught by global handler → HTTP response with descriptive message.

### Frontend Integration

The frontend `getApiErrorMessage()` helper (`lib/utils/error-handler.ts`) tries i18n mapping first, then falls back to displaying the backend's descriptive error message directly.

---

## How to Add New Endpoint

1. Create router file in `routers/` (e.g., `routers/new_feature.py`)
2. Import router into `main.py` and register: `app.include_router(new_feature.router, tags=["new_feature"])`
3. Create service in `new_feature_service.py` with business logic
4. Define request/response schemas in `models.py` (or create `new_feature_models.py`)
5. Implement router functions calling service methods
6. Test with `uv run uvicorn api.main:app --host 0.0.0.0 --port 5055`

## Testing Patterns

- **Interactive docs**: http://localhost:5055/docs (Swagger UI)
- **Direct service tests**: Import service, call methods directly with test data
- **Mock graphs**: Replace graph.ainvoke() with mock for testing service logic
- **Database: Use test database** (separate SurrealDB instance or mock repo_query)

---

## Credential Management (API Configuration UI)

The Credential Management system enables users to configure AI provider credentials through the UI instead of environment variables. Keys are stored securely in SurrealDB (encrypted via Fernet) with database-first fallback to environment variables.

### Router: `routers/credentials.py`

**Endpoints**:

| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/credentials` | List all credentials (optional `?provider=` filter) |
| GET | `/credentials/by-provider/{provider}` | List credentials for a provider |
| POST | `/credentials` | Create a new credential |
| GET | `/credentials/{credential_id}` | Get a specific credential |
| PUT | `/credentials/{credential_id}` | Update a credential |
| DELETE | `/credentials/{credential_id}` | Delete a credential |
| POST | `/credentials/{credential_id}/test` | Test connection using credential |
| POST | `/credentials/{credential_id}/discover` | Discover available models |
| POST | `/credentials/{credential_id}/register-models` | Register discovered models |
| POST | `/credentials/migrate-from-provider-config` | Migrate from legacy ProviderConfig |

**Supported Providers** (13 total):
- Simple API key: `openai`, `anthropic`, `google`, `groq`, `mistral`, `deepseek`, `xai`, `openrouter`, `voyage`, `elevenlabs`
- URL-based: `ollama`
- Multi-field: `azure`, `vertex`, `openai_compatible`

**Security Features**:
- NEVER returns actual API key values (only metadata)
- URL validation (SSRF protection) on all URL fields via `_validate_url()`
- Allows private IPs and localhost for self-hosted services (Ollama, LM Studio)
- Requires `OPEN_NOTEBOOK_ENCRYPTION_KEY` to be set for storing credentials

### Domain Model: `Credential` (`open_notebook/domain/credential.py`)

Individual credential records replacing the old `ProviderConfig` singleton. Each credential stores:
- Provider name, display name, modalities
- Encrypted API key (via Fernet)
- Provider-specific config (base_url, endpoint, api_version, etc.)

### Integration with Key Provider (`open_notebook/ai/key_provider.py`)

The `key_provider` module provisions DB-stored credentials into environment variables for Esperanto compatibility:

**Database-first Pattern**:
1. API endpoint saves keys to `Credential` records (encrypted in SurrealDB)
2. Before model provisioning, `provision_provider_keys(provider)` checks DB, then env vars
3. Keys from DB are set as environment variables for Esperanto compatibility
4. Existing env vars remain unchanged if no DB config exists

**Key Functions**:
- `get_api_key(provider)`: Get API key (DB first, env fallback)
- `provision_provider_keys(provider)`: Set env vars from DB for a provider
- `provision_all_keys()`: Load all provider keys from DB into env vars

### Authentication

No changes to authentication. The `credentials` router uses the same `PasswordAuthMiddleware` as all other endpoints. Keys are protected by the same password-based auth.

**Auth Flow** (unchanged from `api/auth.py`):
- `PasswordAuthMiddleware`: Global middleware checking `Authorization: Bearer {password}` header
- Default password: `open-notebook-change-me` (set `OPEN_NOTEBOOK_PASSWORD` in production)
- Docker secrets support via `OPEN_NOTEBOOK_PASSWORD_FILE`

### Connection Testing (`open_notebook/ai/connection_tester.py`)

The `/credentials/{credential_id}/test` endpoint uses minimal API calls to verify credentials:
- Loads Credential via `Credential.get(config_id)`, uses `credential.to_esperanto_config()`
- Uses cheapest/smallest models per provider (TEST_MODELS map)
- Returns success status and descriptive message
- Special handlers for ollama, openai_compatible, and azure providers

### Migration Workflows

Two migration endpoints help users transition to the credential system:

**From environment variables** (`POST /credentials/migrate-from-env`):
1. Checks each provider for env var presence
2. Creates Credential records from env var values
3. Returns summary: migrated, skipped, errors

**From legacy ProviderConfig** (`POST /credentials/migrate-from-provider-config`):
1. Reads old ProviderConfig records from database
2. Converts each to individual Credential records
3. Returns summary: migrated, skipped, errors

### Example Usage

```python
# Check status
GET /credentials/status
# Response: {"configured": {"openai": true, "anthropic": false}, "source": {"openai": "database", "anthropic": "none"}, "encryption_configured": true}

# Create credential
POST /credentials
{"name": "My OpenAI Key", "provider": "openai", "modalities": ["language", "embedding"], "api_key": "sk-proj-..."}

# Test connection
POST /credentials/{credential_id}/test
# Response: {"provider": "openai", "success": true, "message": "Connection successful"}

# Discover models
POST /credentials/{credential_id}/discover
# Response: {"provider": "openai", "models": [{"model_id": "gpt-4", "name": "gpt-4", ...}], "credential_id": "..."}

# Migrate from env
POST /credentials/migrate-from-env
# Response: {"message": "Migration complete. Migrated 3 providers.", "migrated": ["openai", "anthropic", "groq"], "skipped": [], "errors": []}
```


================================================
FILE: api/__init__.py
================================================


================================================
FILE: api/auth.py
================================================
from typing import Optional

from fastapi import Depends, HTTPException, Request
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from loguru import logger
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import JSONResponse

from open_notebook.utils.encryption import get_secret_from_env


class PasswordAuthMiddleware(BaseHTTPMiddleware):
    """
    Middleware to check password authentication for all API requests.
    Always active with default password if OPEN_NOTEBOOK_PASSWORD is not set.
    Supports Docker secrets via OPEN_NOTEBOOK_PASSWORD_FILE.
    """

    def __init__(self, app, excluded_paths: Optional[list] = None):
        super().__init__(app)
        self.password = get_secret_from_env("OPEN_NOTEBOOK_PASSWORD")
        self.excluded_paths = excluded_paths or [
            "/",
            "/health",
            "/docs",
            "/openapi.json",
            "/redoc",
        ]

    async def dispatch(self, request: Request, call_next):
        # Skip authentication if no password is set
        if not self.password:
            return await call_next(request)

        # Skip authentication for excluded paths
        if request.url.path in self.excluded_paths:
            return await call_next(request)

        # Skip authentication for CORS preflight requests (OPTIONS)
        if request.method == "OPTIONS":
            return await call_next(request)

        # Check authorization header
        auth_header = request.headers.get("Authorization")

        if not auth_header:
            return JSONResponse(
                status_code=401,
                content={"detail": "Missing authorization header"},
                headers={"WWW-Authenticate": "Bearer"},
            )

        # Expected format: "Bearer {password}"
        try:
            scheme, credentials = auth_header.split(" ", 1)
            if scheme.lower() != "bearer":
                raise ValueError("Invalid authentication scheme")
        except ValueError:
            return JSONResponse(
                status_code=401,
                content={"detail": "Invalid authorization header format"},
                headers={"WWW-Authenticate": "Bearer"},
            )

        # Check password
        if credentials != self.password:
            return JSONResponse(
                status_code=401,
                content={"detail": "Invalid password"},
                headers={"WWW-Authenticate": "Bearer"},
            )

        # Password is correct, proceed with the request
        response = await call_next(request)
        return response


# Optional: HTTPBearer security scheme for OpenAPI documentation
security = HTTPBearer(auto_error=False)


def check_api_password(
    credentials: Optional[HTTPAuthorizationCredentials] = Depends(security),
) -> bool:
    """
    Utility function to check API password.
    Can be used as a dependency in individual routes if needed.
    Supports Docker secrets via OPEN_NOTEBOOK_PASSWORD_FILE.
    Returns True without checking credentials if OPEN_NOTEBOOK_PASSWORD is not configured.
    Raises 401 if credentials are missing or don't match the configured password.
    """
    password = get_secret_from_env("OPEN_NOTEBOOK_PASSWORD")

    # No password configured - skip authentication
    if not password:
        return True

    # No credentials provided
    if not credentials:
        raise HTTPException(
            status_code=401,
            detail="Missing authorization",
            headers={"WWW-Authenticate": "Bearer"},
        )

    # Check password
    if credentials.credentials != password:
        raise HTTPException(
            status_code=401,
            detail="Invalid password",
            headers={"WWW-Authenticate": "Bearer"},
        )

    return True


================================================
FILE: api/chat_service.py
================================================
"""
Chat service for API operations.
Provides async interface for chat functionality.
"""

import os
from typing import Any, Dict, List, Optional

import httpx
from loguru import logger


class ChatService:
    """Service for chat-related API operations"""

    def __init__(self):
        self.base_url = os.getenv("API_BASE_URL", "http://127.0.0.1:5055")
        # Add authentication header if password is set
        self.headers = {}
        password = os.getenv("OPEN_NOTEBOOK_PASSWORD")
        if password:
            self.headers["Authorization"] = f"Bearer {password}"

    async def get_sessions(self, notebook_id: str) -> List[Dict[str, Any]]:
        """Get all chat sessions for a notebook"""
        try:
            async with httpx.AsyncClient() as client:
                response = await client.get(
                    f"{self.base_url}/api/chat/sessions",
                    params={"notebook_id": notebook_id},
                    headers=self.headers,
                )
                response.raise_for_status()
                return response.json()
        except Exception as e:
            logger.error(f"Error fetching chat sessions: {str(e)}")
            raise

    async def create_session(
        self,
        notebook_id: str,
        title: Optional[str] = None,
        model_override: Optional[str] = None,
    ) -> Dict[str, Any]:
        """Create a new chat session"""
        try:
            data: Dict[str, Any] = {"notebook_id": notebook_id}
            if title is not None:
                data["title"] = title
            if model_override is not None:
                data["model_override"] = model_override

            async with httpx.AsyncClient() as client:
                response = await client.post(
                    f"{self.base_url}/api/chat/sessions",
                    json=data,
                    headers=self.headers,
                )
                response.raise_for_status()
                return response.json()
        except Exception as e:
            logger.error(f"Error creating chat session: {str(e)}")
            raise

    async def get_session(self, session_id: str) -> Dict[str, Any]:
        """Get a specific session with messages"""
        try:
            async with httpx.AsyncClient() as client:
                response = await client.get(
                    f"{self.base_url}/api/chat/sessions/{session_id}",
                    headers=self.headers,
                )
                response.raise_for_status()
                return response.json()
        except Exception as e:
            logger.error(f"Error fetching session: {str(e)}")
            raise

    async def update_session(
        self,
        session_id: str,
        title: Optional[str] = None,
        model_override: Optional[str] = None,
    ) -> Dict[str, Any]:
        """Update session properties"""
        try:
            data: Dict[str, Any] = {}
            if title is not None:
                data["title"] = title
            if model_override is not None:
                data["model_override"] = model_override

            if not data:
                raise ValueError(
                    "At least one field must be provided to update a session"
                )

            async with httpx.AsyncClient() as client:
                response = await client.put(
                    f"{self.base_url}/api/chat/sessions/{session_id}",
                    json=data,
                    headers=self.headers,
                )
                response.raise_for_status()
                return response.json()
        except Exception as e:
            logger.error(f"Error updating session: {str(e)}")
            raise

    async def delete_session(self, session_id: str) -> Dict[str, Any]:
        """Delete a chat session"""
        try:
            async with httpx.AsyncClient() as client:
                response = await client.delete(
                    f"{self.base_url}/api/chat/sessions/{session_id}",
                    headers=self.headers,
                )
                response.raise_for_status()
                return response.json()
        except Exception as e:
            logger.error(f"Error deleting session: {str(e)}")
            raise

    async def execute_chat(
        self,
        session_id: str,
        message: str,
        context: Dict[str, Any],
        model_override: Optional[str] = None,
    ) -> Dict[str, Any]:
        """Execute a chat request"""
        try:
            data = {"session_id": session_id, "message": message, "context": context}
            if model_override is not None:
                data["model_override"] = model_override

            # Short connect timeout (10s), long read timeout (10 min) for Ollama/local LLMs
            timeout = httpx.Timeout(connect=10.0, read=600.0, write=30.0, pool=10.0)
            async with httpx.AsyncClient(timeout=timeout) as client:
                response = await client.post(
                    f"{self.base_url}/api/chat/execute", json=data, headers=self.headers
                )
                response.raise_for_status()
                return response.json()
        except Exception as e:
            logger.error(f"Error executing chat: {str(e)}")
            raise

    async def build_context(
        self, notebook_id: str, context_config: Dict[str, Any]
    ) -> Dict[str, Any]:
        """Build context for a notebook"""
        try:
            data = {"notebook_id": notebook_id, "context_config": context_config}

            async with httpx.AsyncClient() as client:
                response = await client.post(
                    f"{self.base_url}/api/chat/context", json=data, headers=self.headers
                )
                response.raise_for_status()
                return response.json()
        except Exception as e:
            logger.error(f"Error building context: {str(e)}")
            raise


# Global instance
chat_service = ChatService()


================================================
FILE: api/client.py
================================================
"""
API client for Open Notebook API.
This module provides a client interface to interact with the Open Notebook API.
"""

import os
from typing import Any, Dict, List, Optional, Union

import httpx
from loguru import logger


class APIClient:
    """Client for Open Notebook API."""

    def __init__(self, base_url: Optional[str] = None):
        self.base_url = base_url or os.getenv("API_BASE_URL", "http://127.0.0.1:5055")
        # Timeout increased to 5 minutes (300s) to accommodate slow LLM operations
        # (transformations, insights) on slower hardware (Ollama, LM Studio, remote APIs)
        # Configurable via API_CLIENT_TIMEOUT environment variable (in seconds)
        timeout_str = os.getenv("API_CLIENT_TIMEOUT", "300.0")
        try:
            timeout_value = float(timeout_str)
            # Validate timeout is within reasonable bounds (30s - 3600s / 1 hour)
            if timeout_value < 30:
                logger.warning(
                    f"API_CLIENT_TIMEOUT={timeout_value}s is too low, using minimum of 30s"
                )
                timeout_value = 30.0
            elif timeout_value > 3600:
                logger.warning(
                    f"API_CLIENT_TIMEOUT={timeout_value}s is too high, using maximum of 3600s"
                )
                timeout_value = 3600.0
            self.timeout = timeout_value
        except ValueError:
            logger.error(
                f"Invalid API_CLIENT_TIMEOUT value '{timeout_str}', using default 300s"
            )
            self.timeout = 300.0

        # Add authentication header if password is set
        self.headers = {}
        password = os.getenv("OPEN_NOTEBOOK_PASSWORD")
        if password:
            self.headers["Authorization"] = f"Bearer {password}"

    def _make_request(
        self, method: str, endpoint: str, timeout: Optional[float] = None, **kwargs
    ) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
        """Make HTTP request to the API."""
        url = f"{self.base_url}{endpoint}"
        request_timeout = timeout if timeout is not None else self.timeout

        # Merge headers
        headers = kwargs.get("headers", {})
        headers.update(self.headers)
        kwargs["headers"] = headers

        try:
            with httpx.Client(timeout=request_timeout) as client:
                response = client.request(method, url, **kwargs)
                response.raise_for_status()
                return response.json()
        except httpx.RequestError as e:
            logger.error(f"Request error for {method} {url}: {str(e)}")
            raise ConnectionError(f"Failed to connect to API: {str(e)}")
        except httpx.HTTPStatusError as e:
            logger.error(
                f"HTTP error {e.response.status_code} for {method} {url}: {e.response.text}"
            )
            raise RuntimeError(
                f"API request failed: {e.response.status_code} - {e.response.text}"
            )
        except Exception as e:
            logger.error(f"Unexpected error for {method} {url}: {str(e)}")
            raise

    # Notebooks API methods
    def get_notebooks(
        self, archived: Optional[bool] = None, order_by: str = "updated desc"
    ) -> List[Dict[Any, Any]]:
        """Get all notebooks."""
        params: Dict[str, Any] = {"order_by": order_by}
        if archived is not None:
            params["archived"] = str(archived).lower()

        result = self._make_request("GET", "/api/notebooks", params=params)
        return result if isinstance(result, list) else [result]

    def create_notebook(
        self, name: str, description: str = ""
    ) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
        """Create a new notebook."""
        data = {"name": name, "description": description}
        return self._make_request("POST", "/api/notebooks", json=data)

    def get_notebook(
        self, notebook_id: str
    ) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
        """Get a specific notebook."""
        return self._make_request("GET", f"/api/notebooks/{notebook_id}")

    def update_notebook(
        self, notebook_id: str, **updates
    ) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
        """Update a notebook."""
        return self._make_request("PUT", f"/api/notebooks/{notebook_id}", json=updates)

    def delete_notebook(
        self, notebook_id: str
    ) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
        """Delete a notebook."""
        return self._make_request("DELETE", f"/api/notebooks/{notebook_id}")

    # Search API methods
    def search(
        self,
        query: str,
        search_type: str = "text",
        limit: int = 100,
        search_sources: bool = True,
        search_notes: bool = True,
        minimum_score: float = 0.2,
    ) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
        """Search the knowledge base."""
        data = {
            "query": query,
            "type": search_type,
            "limit": limit,
            "search_sources": search_sources,
            "search_notes": search_notes,
            "minimum_score": minimum_score,
        }
        return self._make_request("POST", "/api/search", json=data)

    def ask_simple(
        self,
        question: str,
        strategy_model: str,
        answer_model: str,
        final_answer_model: str,
    ) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
        """Ask the knowledge base a question (simple, non-streaming)."""
        data = {
            "question": question,
            "strategy_model": strategy_model,
            "answer_model": answer_model,
            "final_answer_model": final_answer_model,
        }
        # Use configured timeout for long-running ask operations
        return self._make_request(
            "POST", "/api/search/ask/simple", json=data, timeout=self.timeout
        )

    # Models API methods
    def get_models(self, model_type: Optional[str] = None) -> List[Dict[Any, Any]]:
        """Get all models with optional type filtering."""
        params = {}
        if model_type:
            params["type"] = model_type
        result = self._make_request("GET", "/api/models", params=params)
        return result if isinstance(result, list) else [result]

    def create_model(
        self, name: str, provider: str, model_type: str
    ) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
        """Create a new model."""
        data = {
            "name": name,
            "provider": provider,
            "type": model_type,
        }
        return self._make_request("POST", "/api/models", json=data)

    def delete_model(
        self, model_id: str
    ) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
        """Delete a model."""
        return self._make_request("DELETE", f"/api/models/{model_id}")

    def get_default_models(self) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
        """Get default model assignments."""
        return self._make_request("GET", "/api/models/defaults")

    def update_default_models(
        self, **defaults
    ) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
        """Update default model assignments."""
        return self._make_request("PUT", "/api/models/defaults", json=defaults)

    # Transformations API methods
    def get_transformations(self) -> List[Dict[Any, Any]]:
        """Get all transformations."""
        result = self._make_request("GET", "/api/transformations")
        return result if isinstance(result, list) else [result]

    def create_transformation(
        self,
        name: str,
        title: str,
        description: str,
        prompt: str,
        apply_default: bool = False,
    ) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
        """Create a new transformation."""
        data = {
            "name": name,
            "title": title,
            "description": description,
            "prompt": prompt,
            "apply_default": apply_default,
        }
        return self._make_request("POST", "/api/transformations", json=data)

    def get_transformation(
        self, transformation_id: str
    ) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
        """Get a specific transformation."""
        return self._make_request("GET", f"/api/transformations/{transformation_id}")

    def update_transformation(
        self, transformation_id: str, **updates
    ) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
        """Update a transformation."""
        return self._make_request(
            "PUT", f"/api/transformations/{transformation_id}", json=updates
        )

    def delete_transformation(
        self, transformation_id: str
    ) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
        """Delete a transformation."""
        return self._make_request("DELETE", f"/api/transformations/{transformation_id}")

    def execute_transformation(
        self, transformation_id: str, input_text: str, model_id: str
    ) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
        """Execute a transformation on input text."""
        data = {
            "transformation_id": transformation_id,
            "input_text": input_text,
            "model_id": model_id,
        }
        # Use configured timeout for transformation operations
        return self._make_request(
            "POST", "/api/transformations/execute", json=data, timeout=self.timeout
        )

    # Notes API methods
    def get_notes(self, notebook_id: Optional[str] = None) -> List[Dict[Any, Any]]:
        """Get all notes with optional notebook filtering."""
        params = {}
        if notebook_id:
            params["notebook_id"] = notebook_id
        result = self._make_request("GET", "/api/notes", params=params)
        return result if isinstance(result, list) else [result]

    def create_note(
        self,
        content: str,
        title: Optional[str] = None,
        note_type: str = "human",
        notebook_id: Optional[str] = None,
    ) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
        """Create a new note."""
        data = {
            "content": content,
            "note_type": note_type,
        }
        if title:
            data["title"] = title
        if notebook_id:
            data["notebook_id"] = notebook_id
        return self._make_request("POST", "/api/notes", json=data)

    def get_note(self, note_id: str) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
        """Get a specific note."""
        return self._make_request("GET", f"/api/notes/{note_id}")

    def update_note(
        self, note_id: str, **updates
    ) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
        """Update a note."""
        return self._make_request("PUT", f"/api/notes/{note_id}", json=updates)

    def delete_note(self, note_id: str) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
        """Delete a note."""
        return self._make_request("DELETE", f"/api/notes/{note_id}")

    # Embedding API methods
    def embed_content(
        self, item_id: str, item_type: str, async_processing: bool = False
    ) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
        """Embed content for vector search."""
        data = {
            "item_id": item_id,
            "item_type": item_type,
            "async_processing": async_processing,
        }
        # Use configured timeout for embedding operations
        return self._make_request("POST", "/api/embed", json=data, timeout=self.timeout)

    def rebuild_embeddings(
        self,
        mode: str = "existing",
        include_sources: bool = True,
        include_notes: bool = True,
        include_insights: bool = True,
    ) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
        """Rebuild embeddings in bulk.

        Note: This operation can take a long time for large databases.
        Consider increasing API_CLIENT_TIMEOUT to 600-900s for bulk rebuilds.
        """
        data = {
            "mode": mode,
            "include_sources": include_sources,
            "include_notes": include_notes,
            "include_insights": include_insights,
        }
        # Use double the configured timeout for bulk rebuild operations (or configured value if already high)
        rebuild_timeout = max(self.timeout, min(self.timeout * 2, 3600.0))
        return self._make_request(
            "POST", "/api/embeddings/rebuild", json=data, timeout=rebuild_timeout
        )

    def get_rebuild_status(
        self, command_id: str
    ) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
        """Get status of a rebuild operation."""
        return self._make_request("GET", f"/api/embeddings/rebuild/{command_id}/status")

    # Settings API methods
    def get_settings(self) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
        """Get all application settings."""
        return self._make_request("GET", "/api/settings")

    def update_settings(
        self, **settings
    ) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
        """Update application settings."""
        return self._make_request("PUT", "/api/settings", json=settings)

    # Context API methods
    def get_notebook_context(
        self, notebook_id: str, context_config: Optional[Dict] = None
    ) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
        """Get context for a notebook."""
        data: Dict[str, Any] = {"notebook_id": notebook_id}
        if context_config:
            data["context_config"] = context_config
        result = self._make_request(
            "POST", f"/api/notebooks/{notebook_id}/context", json=data
        )
        return result if isinstance(result, dict) else {}

    # Sources API methods
    def get_sources(self, notebook_id: Optional[str] = None) -> List[Dict[Any, Any]]:
        """Get all sources with optional notebook filtering."""
        params = {}
        if notebook_id:
            params["notebook_id"] = notebook_id
        result = self._make_request("GET", "/api/sources", params=params)
        return result if isinstance(result, list) else [result]

    def create_source(
        self,
        notebook_id: Optional[str] = None,
        notebooks: Optional[List[str]] = None,
        source_type: str = "text",
        url: Optional[str] = None,
        file_path: Optional[str] = None,
        content: Optional[str] = None,
        title: Optional[str] = None,
        transformations: Optional[List[str]] = None,
        embed: bool = False,
        delete_source: bool = False,
        async_processing: bool = False,
    ) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
        """Create a new source."""
        data = {
            "type": source_type,
            "embed": embed,
            "delete_source": delete_source,
            "async_processing": async_processing,
        }

        # Handle backward compatibility for notebook_id vs notebooks
        if notebooks:
            data["notebooks"] = notebooks
        elif notebook_id:
            data["notebook_id"] = notebook_id
        else:
            raise ValueError("Either notebook_id or notebooks must be provided")

        if url:
            data["url"] = url
        if file_path:
            data["file_path"] = file_path
        if content:
            data["content"] = content
        if title:
            data["title"] = title
        if transformations:
            data["transformations"] = transformations

        # Use configured timeout for source creation (especially PDF processing with OCR)
        return self._make_request(
            "POST", "/api/sources/json", json=data, timeout=self.timeout
        )

    def get_source(self, source_id: str) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
        """Get a specific source."""
        return self._make_request("GET", f"/api/sources/{source_id}")

    def get_source_status(
        self, source_id: str
    ) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
        """Get processing status for a source."""
        return self._make_request("GET", f"/api/sources/{source_id}/status")

    def update_source(
        self, source_id: str, **updates
    ) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
        """Update a source."""
        return self._make_request("PUT", f"/api/sources/{source_id}", json=updates)

    def delete_source(
        self, source_id: str
    ) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
        """Delete a source."""
        return self._make_request("DELETE", f"/api/sources/{source_id}")

    # Insights API methods
    def get_source_insights(self, source_id: str) -> List[Dict[Any, Any]]:
        """Get all insights for a specific source."""
        result = self._make_request("GET", f"/api/sources/{source_id}/insights")
        return result if isinstance(result, list) else [result]

    def get_insight(
        self, insight_id: str
    ) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
        """Get a specific insight."""
        return self._make_request("GET", f"/api/insights/{insight_id}")

    def delete_insight(
        self, insight_id: str
    ) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
        """Delete a specific insight."""
        return self._make_request("DELETE", f"/api/insights/{insight_id}")

    def save_insight_as_note(
        self, insight_id: str, notebook_id: Optional[str] = None
    ) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
        """Convert an insight to a note."""
        data = {}
        if notebook_id:
            data["notebook_id"] = notebook_id
        return self._make_request(
            "POST", f"/api/insights/{insight_id}/save-as-note", json=data
        )

    def create_source_insight(
        self, source_id: str, transformation_id: str, model_id: Optional[str] = None
    ) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
        """Create a new insight for a source by running a transformation."""
        data = {"transformation_id": transformation_id}
        if model_id:
            data["model_id"] = model_id
        return self._make_request(
            "POST", f"/api/sources/{source_id}/insights", json=data
        )

    # Episode Profiles API methods
    def get_episode_profiles(self) -> List[Dict[Any, Any]]:
        """Get all episode profiles."""
        result = self._make_request("GET", "/api/episode-profiles")
        return result if isinstance(result, list) else [result]

    def get_episode_profile(
        self, profile_name: str
    ) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
        """Get a specific episode profile by name."""
        return self._make_request("GET", f"/api/episode-profiles/{profile_name}")

    def create_episode_profile(
        self,
        name: str,
        description: str = "",
        speaker_config: str = "",
        outline_provider: str = "",
        outline_model: str = "",
        transcript_provider: str = "",
        transcript_model: str = "",
        default_briefing: str = "",
        num_segments: int = 5,
    ) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
        """Create a new episode profile."""
        data = {
            "name": name,
            "description": description,
            "speaker_config": speaker_config,
            "outline_provider": outline_provider,
            "outline_model": outline_model,
            "transcript_provider": transcript_provider,
            "transcript_model": transcript_model,
            "default_briefing": default_briefing,
            "num_segments": num_segments,
        }
        return self._make_request("POST", "/api/episode-profiles", json=data)

    def update_episode_profile(
        self, profile_id: str, **updates
    ) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
        """Update an episode profile."""
        return self._make_request(
            "PUT", f"/api/episode-profiles/{profile_id}", json=updates
        )

    def delete_episode_profile(
        self, profile_id: str
    ) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
        """Delete an episode profile."""
        return self._make_request("DELETE", f"/api/episode-profiles/{profile_id}")


# Global client instance
api_client = APIClient()


================================================
FILE: api/command_service.py
================================================
from typing import Any, Dict, List, Optional

from loguru import logger
from surreal_commands import get_command_status, submit_command


class CommandService:
    """Generic service layer for command operations"""

    @staticmethod
    async def submit_command_job(
        module_name: str,  # Actually app_name for surreal-commands
        command_name: str,
        command_args: Dict[str, Any],
        context: Optional[Dict[str, Any]] = None,
    ) -> str:
        """Submit a generic command job for background processing"""
        try:
            # Ensure command modules are imported before submitting
            # This is needed because submit_command validates against local registry
            try:
                import commands.podcast_commands  # noqa: F401
            except ImportError as import_err:
                logger.error(f"Failed to import command modules: {import_err}")
                raise ValueError("Command modules not available")

            # surreal-commands expects: submit_command(app_name, command_name, args)
            cmd_id = submit_command(
                module_name,  # This is actually the app name (e.g., "open_notebook")
                command_name,  # Command name (e.g., "process_text")
                command_args,  # Input data
            )
            # Convert RecordID to string if needed
            if not cmd_id:
                raise ValueError("Failed to get cmd_id from submit_command")
            cmd_id_str = str(cmd_id)
            logger.info(
                f"Submitted command job: {cmd_id_str} for {module_name}.{command_name}"
            )
            return cmd_id_str

        except Exception as e:
            logger.error(f"Failed to submit command job: {e}")
            raise

    @staticmethod
    async def get_command_status(job_id: str) -> Dict[str, Any]:
        """Get status of any command job"""
        try:
            status = await get_command_status(job_id)
            return {
                "job_id": job_id,
                "status": status.status if status else "unknown",
                "result": status.result if status else None,
                "error_message": getattr(status, "error_message", None)
                if status
                else None,
                "created": str(status.created)
                if status and hasattr(status, "created") and status.created
                else None,
                "updated": str(status.updated)
                if status and hasattr(status, "updated") and status.updated
                else None,
                "progress": getattr(status, "progress", None) if status else None,
            }
        except Exception as e:
            logger.error(f"Failed to get command status: {e}")
            raise

    @staticmethod
    async def list_command_jobs(
        module_filter: Optional[str] = None,
        command_filter: Optional[str] = None,
        status_filter: Optional[str] = None,
        limit: int = 50,
    ) -> List[Dict[str, Any]]:
        """List command jobs with optional filtering"""
        # This will be implemented with proper SurrealDB queries
        # For now, return empty list as this is foundation phase
        return []

    @staticmethod
    async def cancel_command_job(job_id: str) -> bool:
        """Cancel a running command job"""
        try:
            # Implementation depends on surreal-commands cancellation support
            # For now, just log the attempt
            logger.info(f"Attempting to cancel job: {job_id}")
            return True
        except Exception as e:
            logger.error(f"Failed to cancel command job: {e}")
            raise


================================================
FILE: api/context_service.py
================================================
"""
Context service layer using API.
"""

from typing import Any, Dict, List, Optional, Union

from loguru import logger

from api.client import api_client


class ContextService:
    """Service layer for context operations using API."""

    def __init__(self):
        logger.info("Using API for context operations")

    def get_notebook_context(
        self, notebook_id: str, context_config: Optional[Dict] = None
    ) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
        """Get context for a notebook."""
        result = api_client.get_notebook_context(
            notebook_id=notebook_id, context_config=context_config
        )
        return result


# Global service instance
context_service = ContextService()


================================================
FILE: api/credentials_service.py
================================================
"""
Credentials Service

Business logic for managing AI provider credentials.
Extracted from the credentials router to follow the service layer pattern.

All functions raise ValueError for business errors (router converts to HTTPException).
"""

import ipaddress
import os
import socket
from typing import Dict, List, Optional
from urllib.parse import urlparse

import httpx
from loguru import logger
from pydantic import SecretStr

from api.models import CredentialResponse
from open_notebook.domain.credential import Credential
from open_notebook.utils.encryption import get_secret_from_env

# =============================================================================
# Constants
# =============================================================================

# Provider environment variable configuration.
# - "required": ALL listed env vars must be set for the provider to be considered configured.
# - "required_any": at least ONE of the listed env vars must be set.
# - "optional": additional env vars used during migration but not required.
PROVIDER_ENV_CONFIG: Dict[str, dict] = {
    "openai": {"required": ["OPENAI_API_KEY"]},
    "anthropic": {"required": ["ANTHROPIC_API_KEY"]},
    "google": {"required_any": ["GOOGLE_API_KEY", "GEMINI_API_KEY"]},
    "groq": {"required": ["GROQ_API_KEY"]},
    "mistral": {"required": ["MISTRAL_API_KEY"]},
    "deepseek": {"required": ["DEEPSEEK_API_KEY"]},
    "xai": {"required": ["XAI_API_KEY"]},
    "openrouter": {"required": ["OPENROUTER_API_KEY"]},
    "voyage": {"required": ["VOYAGE_API_KEY"]},
    "elevenlabs": {"required": ["ELEVENLABS_API_KEY"]},
    "ollama": {"required": ["OLLAMA_API_BASE"]},
    "vertex": {
        "required": ["VERTEX_PROJECT", "VERTEX_LOCATION"],
        "optional": ["GOOGLE_APPLICATION_CREDENTIALS"],
    },
    "azure": {
        "required": ["AZURE_OPENAI_API_KEY", "AZURE_OPENAI_ENDPOINT", "AZURE_OPENAI_API_VERSION"],
        "optional": [
            "AZURE_OPENAI_ENDPOINT_LLM",
            "AZURE_OPENAI_ENDPOINT_EMBEDDING",
            "AZURE_OPENAI_ENDPOINT_STT",
            "AZURE_OPENAI_ENDPOINT_TTS",
        ],
    },
    "openai_compatible": {
        "required_any": ["OPENAI_COMPATIBLE_BASE_URL", "OPENAI_COMPATIBLE_API_KEY"],
    },
}

PROVIDER_MODALITIES: Dict[str, List[str]] = {
    "openai": ["language", "embedding", "speech_to_text", "text_to_speech"],
    "anthropic": ["language"],
    "google": ["language", "embedding"],
    "groq": ["language", "speech_to_text"],
    "mistral": ["language", "embedding"],
    "deepseek": ["language"],
    "xai": ["language"],
    "openrouter": ["language"],
    "voyage": ["embedding"],
    "elevenlabs": ["text_to_speech"],
    "ollama": ["language", "embedding"],
    "vertex": ["language", "embedding"],
    "azure": ["language", "embedding", "speech_to_text", "text_to_speech"],
    "openai_compatible": ["language", "embedding", "speech_to_text", "text_to_speech"],
}


# =============================================================================
# URL Validation (SSRF protection)
# =============================================================================


def validate_url(url: str, provider: str) -> None:
    """
    Validate URL format for API endpoints.

    This is a self-hosted application, so we allow:
    - Private IPs (10.x, 172.16-31.x, 192.168.x) for self-hosted services
    - Localhost for local services (Ollama, LM Studio, etc.)

    We only block:
    - Invalid schemes (must be http or https)
    - Malformed URLs
    - Link-local addresses (169.254.x.x) - used for cloud metadata endpoints
    - Hostnames that resolve to link-local addresses

    Args:
        url: The URL to validate
        provider: The provider name (for logging/context)

    Raises:
        ValueError: If the URL is invalid
    """
    if not url or not url.strip():
        return  # Empty URLs handled elsewhere

    try:
        parsed = urlparse(url.strip())

        # Validate scheme - only http/https allowed
        if parsed.scheme not in ("http", "https"):
            raise ValueError(
                f"Invalid URL scheme: '{parsed.scheme}'. Only http and https are allowed."
            )

        # Extract hostname
        hostname = parsed.hostname
        if not hostname:
            raise ValueError("Invalid URL: hostname could not be determined.")

        # Try to parse as IP address to check for dangerous addresses
        try:
            ip = ipaddress.ip_address(hostname)

            # Block link-local addresses (169.254.x.x) - used for cloud metadata
            # These are dangerous as they can expose cloud instance credentials
            if ip.is_link_local:
                raise ValueError(
                    "Link-local addresses (169.254.x.x) are not allowed for security reasons. "
                    "These addresses are used for cloud metadata endpoints."
                )

            # Block IPv4-mapped IPv6 addresses pointing to link-local
            # e.g. ::ffff:169.254.169.254 bypasses IPv6 is_link_local check
            if hasattr(ip, "ipv4_mapped") and ip.ipv4_mapped and ip.ipv4_mapped.is_link_local:
                raise ValueError(
                    "Link-local addresses (169.254.x.x) are not allowed for security reasons. "
                    "These addresses are used for cloud metadata endpoints."
                )

        except ValueError as ve:
            # Re-raise our own ValueErrors
            if "Link-local" in str(ve) or "Invalid URL" in str(ve):
                raise
            # Not an IP address, it's a hostname - need to resolve and check
            try:
                # Resolve hostname to IP address
                resolved_ips = socket.getaddrinfo(hostname, None)
                for family, _, _, _, sockaddr in resolved_ips:
                    ip_addr = sockaddr[0]
                    try:
                        parsed_ip = ipaddress.ip_address(ip_addr)
                        if parsed_ip.is_link_local:
                            raise ValueError(
                                f"Hostname '{hostname}' resolves to a link-local address (169.254.x.x) which is not allowed for security reasons. "
                                "These addresses are used for cloud metadata endpoints."
                            )
                        # Block IPv4-mapped IPv6 addresses pointing to link-local
                        if (
                            hasattr(parsed_ip, "ipv4_mapped")
                            and parsed_ip.ipv4_mapped
                            and parsed_ip.ipv4_mapped.is_link_local
                        ):
                            raise ValueError(
                                f"Hostname '{hostname}' resolves to a link-local address (169.254.x.x) which is not allowed for security reasons. "
                                "These addresses are used for cloud metadata endpoints."
                            )
                    except ValueError as inner_ve:
                        if "link-local" in str(inner_ve).lower() or "Link-local" in str(inner_ve):
                            raise
                        # Skip non-IP addresses (e.g., IPv6 zones)
                        continue
            except socket.gaierror:
                # Could not resolve hostname - allow it since the URL may be
                # valid in the deployment environment (e.g., Azure endpoints,
                # internal DNS names). We only block link-local addresses.
                pass

    except ValueError:
        raise
    except Exception:
        raise ValueError("Invalid URL format. Check server logs for details.")


# =============================================================================
# Helpers
# =============================================================================


def require_encryption_key() -> None:
    """Raise ValueError if encryption key is not configured."""
    if not get_secret_from_env("OPEN_NOTEBOOK_ENCRYPTION_KEY"):
        raise ValueError(
            "Encryption key not configured. "
            "Set OPEN_NOTEBOOK_ENCRYPTION_KEY to enable storing API keys."
        )


def credential_to_response(cred: Credential, model_count: int = 0) -> CredentialResponse:
    """Convert a Credential domain object to API response."""
    return CredentialResponse(
        id=cred.id or "",
        name=cred.name,
        provider=cred.provider,
        modalities=cred.modalities,
        base_url=cred.base_url,
        endpoint=cred.endpoint,
        api_version=cred.api_version,
        endpoint_llm=cred.endpoint_llm,
        endpoint_embedding=cred.endpoint_embedding,
        endpoint_stt=cred.endpoint_stt,
        endpoint_tts=cred.endpoint_tts,
        project=cred.project,
        location=cred.location,
        credentials_path=cred.credentials_path,
        has_api_key=cred.api_key is not None,
        created=str(cred.created) if cred.created else "",
        updated=str(cred.updated) if cred.updated else "",
        model_count=model_count,
    )


def check_env_configured(provider: str) -> bool:
    """Check if a provider has sufficient env vars configured for migration."""
    config = PROVIDER_ENV_CONFIG.get(provider)
    if not config:
        return False

    if "required_any" in config:
        return any(bool(os.environ.get(v, "").strip()) for v in config["required_any"])
    elif "required" in config:
        return all(bool(os.environ.get(v, "").strip()) for v in config["required"])
    return False


def get_default_modalities(provider: str) -> List[str]:
    """Get default modalities for a provider."""
    return PROVIDER_MODALITIES.get(provider.lower(), ["language"])


def create_credential_from_env(provider: str) -> Credential:
    """Create a Credential from environment variables for a given provider."""
    modalities = get_default_modalities(provider)
    name = "Default (Migrated from env)"

    if provider == "ollama":
        return Credential(
            name=name,
            provider=provider,
            modalities=modalities,
            base_url=os.environ.get("OLLAMA_API_BASE"),
        )
    elif provider == "vertex":
        return Credential(
            name=name,
            provider=provider,
            modalities=modalities,
            project=os.environ.get("VERTEX_PROJECT"),
            location=os.environ.get("VERTEX_LOCATION"),
            credentials_path=os.environ.get("GOOGLE_APPLICATION_CREDENTIALS"),
        )
    elif provider == "azure":
        return Credential(
            name=name,
            provider=provider,
            modalities=modalities,
            api_key=SecretStr(os.environ["AZURE_OPENAI_API_KEY"]),
            endpoint=os.environ.get("AZURE_OPENAI_ENDPOINT"),
            api_version=os.environ.get("AZURE_OPENAI_API_VERSION"),
            endpoint_llm=os.environ.get("AZURE_OPENAI_ENDPOINT_LLM"),
            endpoint_embedding=os.environ.get("AZURE_OPENAI_ENDPOINT_EMBEDDING"),
            endpoint_stt=os.environ.get("AZURE_OPENAI_ENDPOINT_STT"),
            endpoint_tts=os.environ.get("AZURE_OPENAI_ENDPOINT_TTS"),
        )
    elif provider == "openai_compatible":
        api_key = os.environ.get("OPENAI_COMPATIBLE_API_KEY")
        return Credential(
            name=name,
            provider=provider,
            modalities=modalities,
            api_key=SecretStr(api_key) if api_key else None,
            base_url=os.environ.get("OPENAI_COMPATIBLE_BASE_URL"),
        )
    elif provider == "google":
        # Support both GOOGLE_API_KEY and GEMINI_API_KEY (fallback)
        api_key = os.environ.get("GOOGLE_API_KEY") or os.environ.get("GEMINI_API_KEY")
        return Credential(
            name=name,
            provider=provider,
            modalities=modalities,
            api_key=SecretStr(api_key) if api_key else None,
        )
    else:
        # Simple API key providers
        config = PROVIDER_ENV_CONFIG.get(provider, {})
        required = config.get("required", [])
        env_var = required[0] if required else None
        api_key = os.environ.get(env_var) if env_var else None
        return Credential(
            name=name,
            provider=provider,
            modalities=modalities,
            api_key=SecretStr(api_key) if api_key else None,
        )


# =============================================================================
# Service Functions
# =============================================================================


async def get_provider_status() -> dict:
    """
    Get configuration status: encryption key status, and per-provider
    configured/source information.
    """
    encryption_configured = bool(get_secret_from_env("OPEN_NOTEBOOK_ENCRYPTION_KEY"))

    configured: Dict[str, bool] = {}
    source: Dict[str, str] = {}

    for provider in PROVIDER_ENV_CONFIG:
        env_configured = check_env_configured(provider)
        try:
            db_credentials = await Credential.get_by_provider(provider)
            db_configured = len(db_credentials) > 0
        except Exception:
            db_configured = False

        configured[provider] = db_configured or env_configured

        if db_configured:
            source[provider] = "database"
        elif env_configured:
            source[provider] = "environment"
        else:
            source[provider] = "none"

    return {
        "configured": configured,
        "source": source,
        "encryption_configured": encryption_configured,
    }


async def get_env_status() -> Dict[str, bool]:
    """Check what's configured via environment variables."""
    env_status: Dict[str, bool] = {}
    for provider in PROVIDER_ENV_CONFIG:
        env_status[provider] = check_env_configured(provider)
    return env_status


async def test_credential(credential_id: str) -> dict:
    """
    Test connection using a credential's configuration.

    Returns dict with provider, success, message keys.
    """
    provider = "unknown"
    try:
        cred = await Credential.get(credential_id)
        config = cred.to_esperanto_config()

        from open_notebook.ai.connection_tester import (
            _test_azure_connection,
            _test_ollama_connection,
            _test_openai_compatible_connection,
        )

        provider = cred.provider.lower()

        # Handle special providers
        if provider == "ollama":
            base_url = config.get("base_url", "http://localhost:11434")
            success, message = await _test_ollama_connection(base_url)
            return {"provider": provider, "success": success, "message": message}

        if provider == "openai_compatible":
            base_url = config.get("base_url")
            api_key = config.get("api_key")
            if not base_url:
                return {
                    "provider": provider,
                    "success": False,
                    "message": "No base URL configured",
                }
            success, message = await _test_openai_compatible_connection(
                base_url, api_key
            )
            return {"provider": provider, "success": success, "message": message}

        if provider == "azure":
            success, message = await _test_azure_connection(
                endpoint=config.get("endpoint"),
                api_key=config.get("api_key"),
                api_version=config.get("api_version"),
            )
            return {"provider": provider, "success": success, "message": message}

        # Standard provider: use Esperanto to create and test
        from esperanto.factory import AIFactory

        from open_notebook.ai.connection_tester import TEST_MODELS

        if provider not in TEST_MODELS:
            return {
                "provider": provider,
                "success": False,
                "message": f"Unknown provider: {provider}",
            }

        test_model, test_type = TEST_MODELS[provider]
        if not test_model:
            return {
                "provider": provider,
                "success": False,
                "message": f"No test model configured for {provider}",
            }

        if test_type == "language":
            model = AIFactory.create_language(
                model_name=test_model, provider=provider, config=config
            )
            lc_model = model.to_langchain()
            await lc_model.ainvoke("Hi")
            return {"provider": provider, "success": True, "message": "Connection successful"}

        elif test_type == "embedding":
            model = AIFactory.create_embedding(
                model_name=test_model, provider=provider, config=config
            )
            await model.aembed(["test"])
            return {"provider": provider, "success": True, "message": "Connection successful"}

        elif test_type == "text_to_speech":
            AIFactory.create_text_to_speech(model_name=test_model, provider=provider, config=config)
            return {
                "provider": provider,
                "success": True,
                "message": "Connection successful (key format valid)",
            }

        return {
            "provider": provider,
            "success": False,
            "message": f"Unsupported test type: {test_type}",
        }

    except Exception as e:
        error_msg = str(e)
        if "401" in error_msg or "unauthorized" in error_msg.lower():
            return {"provider": provider, "success": False, "message": "Invalid API key"}
        elif "403" in error_msg or "forbidden" in error_msg.lower():
            return {"provider": provider, "success": False, "message": "API key lacks required permissions"}
        elif "rate" in error_msg.lower() and "limit" in error_msg.lower():
            return {"provider": provider, "success": True, "message": "Rate limited - but connection works"}
        elif "not found" in error_msg.lower() and "model" in error_msg.lower():
            return {"provider": provider, "success": True, "message": "API key valid (test model not available)"}
        else:
            logger.debug(f"Test connection error for credential {credential_id}: {e}")
            truncated = error_msg[:100] + "..." if len(error_msg) > 100 else error_msg
            return {"provider": provider, "success": False, "message": f"Error: {truncated}"}


async def discover_with_config(provider: str, config: dict) -> List[dict]:
    """
    Discover models using explicit config instead of env vars.

    Returns model names only — no type classification.
    The user chooses the model type when registering.
    """
    api_key = config.get("api_key")
    base_url = config.get("base_url")

    # Static model lists for providers without a listing API
    STATIC_MODELS: Dict[str, List[str]] = {
        "anthropic": [
            "claude-opus-4-20250514",
            "claude-sonnet-4-20250514",
            "claude-3-5-sonnet-20241022",
            "claude-3-5-haiku-20241022",
            "claude-3-opus-20240229",
            "claude-3-sonnet-20240229",
            "claude-3-haiku-20240307",
        ],
        "voyage": [
            "voyage-3", "voyage-3-lite", "voyage-code-3",
            "voyage-finance-2", "voyage-law-2", "voyage-multilingual-2",
        ],
        "elevenlabs": [
            "eleven_multilingual_v2", "eleven_turbo_v2_5",
            "eleven_turbo_v2", "eleven_monolingual_v1",
        ],
    }

    if provider in STATIC_MODELS:
        if not api_key and provider != "ollama":
            return []
        return [
            {"name": m, "provider": provider}
            for m in STATIC_MODELS[provider]
        ]

    # API-based discovery URLs (OpenAI-style /models endpoints)
    url_map = {
        "openai": "https://api.openai.com/v1/models",
        "groq": "https://api.groq.com/openai/v1/models",
        "mistral": "https://api.mistral.ai/v1/models",
        "deepseek": "https://api.deepseek.com/models",
        "xai": "https://api.x.ai/v1/models",
        "openrouter": "https://openrouter.ai/api/v1/models",
    }

    if provider == "ollama":
        ollama_url = base_url or "http://localhost:11434"
        try:
            async with httpx.AsyncClient() as client:
                response = await client.get(f"{ollama_url}/api/tags", timeout=10.0)
                response.raise_for_status()
                data = response.json()
                return [
                    {"name": m.get("name", ""), "provider": "ollama"}
                    for m in data.get("models", [])
                    if m.get("name")
                ]
        except Exception as e:
            logger.warning(f"Failed to discover Ollama models: {e}")
            return []

    if provider == "openai_compatible":
        if not base_url:
            return []
        try:
            headers = {}
            if api_key:
                headers["Authorization"] = f"Bearer {api_key}"
            async with httpx.AsyncClient() as client:
                response = await client.get(
                    f"{base_url.rstrip('/')}/models", headers=headers, timeout=30.0,
                )
                response.raise_for_status()
                data = response.json()
                return [
                    {"name": m.get("id", ""), "provider": "openai_compatible"}
                    for m in data.get("data", [])
                    if m.get("id")
                ]
        except Exception as e:
            logger.warning(f"Failed to discover openai_compatible models: {e}")
            return []

    if provider == "azure":
        endpoint = config.get("endpoint")
        api_version = config.get("api_version", "2024-10-21")
        if not endpoint or not api_key:
            return []
        try:
            url = f"{endpoint.rstrip('/')}/openai/models?api-version={api_version}"
            headers = {"api-key": api_key}
            async with httpx.AsyncClient() as client:
                response = await client.get(url, headers=headers, timeout=30.0)
                response.raise_for_status()
                data = response.json()
                return [
                    {"name": m.get("id", ""), "provider": "azure"}
                    for m in data.get("data", [])
                    if m.get("id")
                ]
        except Exception as e:
            logger.warning(f"Failed to discover Azure models: {e}")
            return []

    if provider == "vertex":
        # Vertex AI requires service-account OAuth2 for model listing.
        # Return a curated static list of well-known Vertex models instead.
        VERTEX_MODELS = [
            "gemini-2.0-flash",
            "gemini-2.0-flash-lite",
            "gemini-1.5-pro",
            "gemini-1.5-flash",
            "text-embedding-005",
        ]
        return [{"name": m, "provider": "vertex"} for m in VERTEX_MODELS]

    if provider == "google":
        try:
            headers = {"X-Goog-Api-Key": api_key} if api_key else {}
            async with httpx.AsyncClient() as client:
                response = await client.get(
                    "https://generativelanguage.googleapis.com/v1/models",
                    headers=headers,
                    timeout=30.0,
                )
                response.raise_for_status()
                data = response.json()
                return [
                    {
                        "name": model.get("name", "").replace("models/", ""),
                        "provider": "google",
                        "description": model.get("displayName"),
                    }
                    for model in data.get("models", [])
                    if model.get("name")
                ]
        except Exception as e:
            logger.warning(f"Failed to discover Google models: {e}")
            return []

    # Standard OpenAI-style API discovery
    discovery_url = url_map.get(provider)
    if not discovery_url or not api_key:
        return []

    try:
        async with httpx.AsyncClient() as client:
            response = await client.get(
                discovery_url,
                headers={"Authorization": f"Bearer {api_key}"},
                timeout=30.0,
            )
            response.raise_for_status()
            data = response.json()

            return [
                {
                    "name": m.get("id", ""),
                    "provider": provider,
                    "description": m.get("name"),
                }
                for m in data.get("data", [])
                if m.get("id")
            ]
    except Exception as e:
        logger.warning(f"Failed to discover {provider} models: {e}")
        return []


async def register_models(credential_id: str, models_data: list) -> dict:
    """
    Register discovered models and link them to a credential.

    Args:
        credential_id: The credential ID to link models to
        models_data: List of dicts with name, provider, model_type

    Returns:
        dict with created and existing counts
    """
    cred = await Credential.get(credential_id)

    from open_notebook.ai.models import Model
    from open_notebook.database.repository import repo_query

    # Batch fetch existing models for this provider
    existing_models = await repo_query(
        "SELECT string::lowercase(name) as name, string::lowercase(type) as type FROM model "
        "WHERE string::lowercase(provider) = $provider",
        {"provider": cred.provider.lower()},
    )
    existing_keys = {(m["name"], m["type"]) for m in existing_models}

    created = 0
    existing = 0

    for model_data in models_data:
        key = (model_data.name.lower(), model_data.model_type.lower())
        if key in existing_keys:
            existing += 1
            continue

        new_model = Model(
            name=model_data.name,
            provider=model_data.provider or cred.provider,
            type=model_data.model_type,
            credential=cred.id,
        )
        await new_model.save()
        created += 1

    return {"created": created, "existing": existing}


async def migrate_from_provider_config() -> dict:
    """
    Migrate existing ProviderConfig data to individual credential records.

    Returns dict with message, migrated, skipped, errors.
    """
    logger.info("=== Starting ProviderConfig migration ===")

    require_encryption_key()
    logger.info("Encryption key verified")

    from open_notebook.domain.provider_config import ProviderConfig

    config = await ProviderConfig.get_instance()
    logger.info(
        f"Found ProviderConfig with {len(config.credentials)} provider(s): "
        f"{', '.join(config.credentials.keys())}"
    )

    migrated = []
    skipped = []
    errors = []

    for provider, credentials_list in config.credentials.items():
        for old_cred in credentials_list:
            try:
                # Check if a credential already exists for this provider with same name
                existing = await Credential.get_by_provider(provider)
                names = [c.name for c in existing]
                if old_cred.name in names:
                    logger.info(
                        f"[{provider}/{old_cred.name}] Already exists in DB, skipping"
                    )
                    skipped.append(f"{provider}/{old_cred.name}")
                    continue

                # Determine modalities from the provider type
                modalities = get_default_modalities(provider)

                logger.info(f"[{provider}/{old_cred.name}] Creating credential")
                new_cred = Credential(
                    name=old_cred.name,
                    provider=provider,
                    modalities=modalities,
                    api_key=old_cred.api_key,
                    base_url=old_cred.base_url,
                    endpoint=old_cred.endpoint,
                    api_version=old_cred.api_version,
                    endpoint_llm=old_cred.endpoint_llm,
                    endpoint_embedding=old_cred.endpoint_embedding,
                    endpoint_stt=old_cred.endpoint_stt,
                    endpoint_tts=old_cred.endpoint_tts,
                    project=old_cred.project,
                    location=old_cred.location,
                    credentials_path=old_cred.credentials_path,
                )
                await new_cred.save()
                logger.info(
                    f"[{provider}/{old_cred.name}] Credential saved (id={new_cred.id})"
                )

                # Link existing models for this provider to the new credential
                from open_notebook.ai.models import Model
                from open_notebook.database.repository import repo_query

                provider_models = await repo_query(
                    "SELECT * FROM model WHERE string::lowercase(provider) = $provider AND credential IS NONE",
                    {"provider": provider.lower()},
                )
                if provider_models:
                    logger.info(
                        f"[{provider}/{old_cred.name}] Linking {len(provider_models)} "
                        f"unassigned model(s)"
                    )
                    for model_data in provider_models:
                        model = Model(**model_data)
                        model.credential = new_cred.id
                        await model.save()

                migrated.append(f"{provider}/{old_cred.name}")

            except Exception as e:
                logger.error(
                    f"[{provider}/{old_cred.name}] Migration FAILED: "
                    f"{type(e).__name__}: {e}",
                    exc_info=True,
                )
                errors.append(f"{provider}/{old_cred.name}: {e}")

    logger.info(
        f"=== ProviderConfig migration complete === "
        f"migrated={len(migrated)} skipped={len(skipped)} errors={len(errors)}"
    )
    if migrated:
        logger.info(f"  Migrated: {', '.join(migrated)}")
    if skipped:
        logger.info(f"  Skipped: {', '.join(skipped)}")
    if errors:
        logger.error(f"  Errors: {'; '.join(errors)}")

    return {
        "message": f"Migration complete. Migrated {len(migrated)} credentials.",
        "migrated": migrated,
        "skipped": skipped,
        "errors": errors,
    }


async def migrate_from_env() -> dict:
    """
    Migrate API keys from environment variables to credential records.

    Returns dict with message, migrated, skipped, not_configured, errors.
    """
    logger.info("=== Starting environment variable migration ===")
    logger.info(
        f"Checking {len(PROVIDER_ENV_CONFIG)} providers: "
        f"{', '.join(PROVIDER_ENV_CONFIG.keys())}"
    )

    require_encryption_key()
    logger.info("Encryption key verified")

    from open_notebook.ai.models import Model
    from open_notebook.database.repository import repo_query

    migrated = []
    skipped = []
    not_configured = []
    errors = []

    for provider in PROVIDER_ENV_CONFIG:
        try:
            if not check_env_configured(provider):
                logger.debug(f"[{provider}] No env vars configured, skipping")
                not_configured.append(provider)
                continue

            logger.info(f"[{provider}] Env vars detected, checking for existing credentials")

            existing = await Credential.get_by_provider(provider)
            if existing:
                logger.info(
                    f"[{provider}] Already has {len(existing)} credential(s) in DB, skipping"
                )
                skipped.append(provider)
                continue

            logger.info(f"[{provider}] Creating credential from env vars")
            cred = create_credential_from_env(provider)
            await cred.save()
            logger.info(f"[{provider}] Credential saved successfully (id={cred.id})")

            # Link unassigned models to this credential
            provider_models = await repo_query(
                "SELECT * FROM model WHERE string::lowercase(provider) = $provider AND credential IS NONE",
                {"provider": provider.lower()},
            )
            if provider_models:
                logger.info(
                    f"[{provider}] Linking {len(provider_models)} unassigned model(s) "
                    f"to credential {cred.id}"
                )
                for model_data in provider_models:
                    model = Model(**model_data)
                    model.credential = cred.id
                    await model.save()
            else:
                logger.info(f"[{provider}] No unassigned models to link")

            migrated.append(provider)

        except Exception as e:
            logger.error(
                f"[{provider}] Migration FAILED: {type(e).__name__}: {e}",
                exc_info=True,
            )
            errors.append(f"{provider}: {e}")

    logger.info(
        f"=== Environment variable migration complete === "
        f"migrated={len(migrated)} skipped={len(skipped)} "
        f"not_configured={len(not_configured)} errors={len(errors)}"
    )
    if migrated:
        logger.info(f"  Migrated: {', '.join(migrated)}")
    if skipped:
        logger.info(f"  Skipped (already in DB): {', '.join(skipped)}")
    if errors:
        logger.error(f"  Errors: {'; '.join(errors)}")

    return {
        "message": f"Migration complete. Migrated {len(migrated)} providers.",
        "migrated": migrated,
        "skipped": skipped,
        "not_configured": not_configured,
        "errors": errors,
    }


================================================
FILE: api/embedding_service.py
================================================
"""
Embedding service layer using API.
"""

from typing import Any, Dict, List, Union

from loguru import logger

from api.client import api_client


class EmbeddingService:
    """Service layer for embedding operations using API."""

    def __init__(self):
        logger.info("Using API for embedding operations")

    def embed_content(
        self, item_id: str, item_type: str
    ) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
        """Embed content for vector search."""
        result = api_client.embed_content(item_id=item_id, item_type=item_type)
        return result


# Global service instance
embedding_service = EmbeddingService()


================================================
FILE: api/episode_profiles_service.py
================================================
"""
Episode profiles service layer using API.
"""

from typing import List

from loguru import logger

from api.client import api_client
from open_notebook.podcasts.models import EpisodeProfile


class EpisodeProfilesService:
    """Service layer for episode profiles operations using API."""

    def __init__(self):
        logger.info("Using API for episode profiles operations")

    def get_all_episode_profiles(self
Download .txt
gitextract_5wx36h6b/

├── .dockerignore
├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.yml
│   │   ├── config.yml
│   │   ├── feature_request.yml
│   │   └── installation_issue.yml
│   ├── pull_request_template.md
│   └── workflows/
│       ├── build-and-release.yml
│       ├── build-dev.yml
│       ├── claude-code-review.yml
│       ├── claude.yml
│       └── test.yml
├── .gitignore
├── .python-version
├── .worktreeinclude
├── CHANGELOG.md
├── CLAUDE.md
├── CONFIGURATION.md
├── CONTRIBUTING.md
├── Dockerfile
├── Dockerfile.single
├── LICENSE
├── MAINTAINER_GUIDE.md
├── Makefile
├── README.dev.md
├── README.md
├── api/
│   ├── CLAUDE.md
│   ├── __init__.py
│   ├── auth.py
│   ├── chat_service.py
│   ├── client.py
│   ├── command_service.py
│   ├── context_service.py
│   ├── credentials_service.py
│   ├── embedding_service.py
│   ├── episode_profiles_service.py
│   ├── insights_service.py
│   ├── main.py
│   ├── models.py
│   ├── models_service.py
│   ├── notebook_service.py
│   ├── notes_service.py
│   ├── podcast_api_service.py
│   ├── podcast_service.py
│   ├── routers/
│   │   ├── __init__.py
│   │   ├── auth.py
│   │   ├── chat.py
│   │   ├── commands.py
│   │   ├── config.py
│   │   ├── context.py
│   │   ├── credentials.py
│   │   ├── embedding.py
│   │   ├── embedding_rebuild.py
│   │   ├── episode_profiles.py
│   │   ├── insights.py
│   │   ├── languages.py
│   │   ├── models.py
│   │   ├── notebooks.py
│   │   ├── notes.py
│   │   ├── podcasts.py
│   │   ├── search.py
│   │   ├── settings.py
│   │   ├── source_chat.py
│   │   ├── sources.py
│   │   ├── speaker_profiles.py
│   │   └── transformations.py
│   ├── search_service.py
│   ├── settings_service.py
│   ├── sources_service.py
│   └── transformations_service.py
├── commands/
│   ├── CLAUDE.md
│   ├── __init__.py
│   ├── embedding_commands.py
│   ├── example_commands.py
│   ├── podcast_commands.py
│   └── source_commands.py
├── docker-compose.yml
├── docs/
│   ├── 0-START-HERE/
│   │   ├── index.md
│   │   ├── quick-start-cloud.md
│   │   ├── quick-start-local.md
│   │   └── quick-start-openai.md
│   ├── 1-INSTALLATION/
│   │   ├── docker-compose.md
│   │   ├── from-source.md
│   │   ├── index.md
│   │   └── single-container.md
│   ├── 2-CORE-CONCEPTS/
│   │   ├── ai-context-rag.md
│   │   ├── chat-vs-transformations.md
│   │   ├── index.md
│   │   ├── notebooks-sources-notes.md
│   │   └── podcasts-explained.md
│   ├── 3-USER-GUIDE/
│   │   ├── adding-sources.md
│   │   ├── api-configuration.md
│   │   ├── chat-effectively.md
│   │   ├── citations.md
│   │   ├── creating-podcasts.md
│   │   ├── index.md
│   │   ├── interface-overview.md
│   │   ├── search.md
│   │   ├── transformations.md
│   │   └── working-with-notes.md
│   ├── 4-AI-PROVIDERS/
│   │   └── index.md
│   ├── 5-CONFIGURATION/
│   │   ├── advanced.md
│   │   ├── ai-providers.md
│   │   ├── database.md
│   │   ├── environment-reference.md
│   │   ├── index.md
│   │   ├── local-stt.md
│   │   ├── local-tts.md
│   │   ├── mcp-integration.md
│   │   ├── ollama.md
│   │   ├── openai-compatible.md
│   │   ├── reverse-proxy.md
│   │   └── security.md
│   ├── 6-TROUBLESHOOTING/
│   │   ├── ai-chat-issues.md
│   │   ├── connection-issues.md
│   │   ├── faq.md
│   │   ├── index.md
│   │   └── quick-fixes.md
│   ├── 7-DEVELOPMENT/
│   │   ├── api-reference.md
│   │   ├── architecture.md
│   │   ├── code-standards.md
│   │   ├── contributing.md
│   │   ├── design-principles.md
│   │   ├── development-setup.md
│   │   ├── index.md
│   │   ├── maintainer-guide.md
│   │   ├── quick-start.md
│   │   └── testing.md
│   ├── SECURITY_REVIEW.md
│   └── index.md
├── examples/
│   ├── README.md
│   ├── docker-compose-dev.yml
│   ├── docker-compose-full-local.yml
│   ├── docker-compose-ollama.yml
│   ├── docker-compose-single.yml
│   └── docker-compose-speaches.yml
├── frontend/
│   ├── .gitignore
│   ├── components.json
│   ├── eslint.config.mjs
│   ├── next.config.ts
│   ├── package.json
│   ├── postcss.config.mjs
│   ├── src/
│   │   ├── CLAUDE.md
│   │   ├── app/
│   │   │   ├── (auth)/
│   │   │   │   └── login/
│   │   │   │       └── page.tsx
│   │   │   ├── (dashboard)/
│   │   │   │   ├── advanced/
│   │   │   │   │   ├── components/
│   │   │   │   │   │   ├── RebuildEmbeddings.tsx
│   │   │   │   │   │   └── SystemInfo.tsx
│   │   │   │   │   └── page.tsx
│   │   │   │   ├── layout.tsx
│   │   │   │   ├── notebooks/
│   │   │   │   │   ├── [id]/
│   │   │   │   │   │   └── page.tsx
│   │   │   │   │   ├── components/
│   │   │   │   │   │   ├── ChatColumn.test.tsx
│   │   │   │   │   │   ├── ChatColumn.tsx
│   │   │   │   │   │   ├── NoteEditorDialog.tsx
│   │   │   │   │   │   ├── NotebookCard.tsx
│   │   │   │   │   │   ├── NotebookDeleteDialog.tsx
│   │   │   │   │   │   ├── NotebookHeader.tsx
│   │   │   │   │   │   ├── NotebookList.tsx
│   │   │   │   │   │   ├── NotesColumn.tsx
│   │   │   │   │   │   └── SourcesColumn.tsx
│   │   │   │   │   └── page.tsx
│   │   │   │   ├── page.tsx
│   │   │   │   ├── podcasts/
│   │   │   │   │   └── page.tsx
│   │   │   │   ├── search/
│   │   │   │   │   └── page.tsx
│   │   │   │   ├── settings/
│   │   │   │   │   ├── api-keys/
│   │   │   │   │   │   └── page.tsx
│   │   │   │   │   ├── components/
│   │   │   │   │   │   └── SettingsForm.tsx
│   │   │   │   │   └── page.tsx
│   │   │   │   ├── sources/
│   │   │   │   │   ├── [id]/
│   │   │   │   │   │   └── page.tsx
│   │   │   │   │   └── page.tsx
│   │   │   │   └── transformations/
│   │   │   │       ├── components/
│   │   │   │       │   ├── DefaultPromptEditor.tsx
│   │   │   │       │   ├── TransformationCard.tsx
│   │   │   │       │   ├── TransformationEditorDialog.tsx
│   │   │   │       │   ├── TransformationPlayground.tsx
│   │   │   │       │   └── TransformationsList.tsx
│   │   │   │       └── page.tsx
│   │   │   ├── config/
│   │   │   │   └── route.ts
│   │   │   ├── globals.css
│   │   │   ├── layout.tsx
│   │   │   └── page.tsx
│   │   ├── components/
│   │   │   ├── auth/
│   │   │   │   └── LoginForm.tsx
│   │   │   ├── common/
│   │   │   │   ├── CommandPalette.tsx
│   │   │   │   ├── ConfirmDialog.test.tsx
│   │   │   │   ├── ConfirmDialog.tsx
│   │   │   │   ├── ConnectionGuard.tsx
│   │   │   │   ├── ContextIndicator.tsx
│   │   │   │   ├── ContextToggle.tsx
│   │   │   │   ├── EmptyState.tsx
│   │   │   │   ├── ErrorBoundary.tsx
│   │   │   │   ├── InlineEdit.tsx
│   │   │   │   ├── LanguageLoadingOverlay.tsx
│   │   │   │   ├── LanguageToggle.tsx
│   │   │   │   ├── LoadingSpinner.tsx
│   │   │   │   ├── ModelSelector.tsx
│   │   │   │   └── ThemeToggle.tsx
│   │   │   ├── errors/
│   │   │   │   └── ConnectionErrorOverlay.tsx
│   │   │   ├── layout/
│   │   │   │   ├── AppShell.tsx
│   │   │   │   ├── AppSidebar.test.tsx
│   │   │   │   ├── AppSidebar.tsx
│   │   │   │   └── SetupBanner.tsx
│   │   │   ├── notebooks/
│   │   │   │   ├── CollapsibleColumn.tsx
│   │   │   │   └── CreateNotebookDialog.tsx
│   │   │   ├── podcasts/
│   │   │   │   ├── EpisodeCard.tsx
│   │   │   │   ├── EpisodeProfilesPanel.tsx
│   │   │   │   ├── EpisodesTab.tsx
│   │   │   │   ├── GeneratePodcastDialog.tsx
│   │   │   │   ├── SpeakerProfilesPanel.tsx
│   │   │   │   ├── TemplatesTab.tsx
│   │   │   │   └── forms/
│   │   │   │       ├── EpisodeProfileFormDialog.tsx
│   │   │   │       └── SpeakerProfileFormDialog.tsx
│   │   │   ├── providers/
│   │   │   │   ├── I18nProvider.tsx
│   │   │   │   ├── ModalProvider.tsx
│   │   │   │   ├── QueryProvider.tsx
│   │   │   │   └── ThemeProvider.tsx
│   │   │   ├── search/
│   │   │   │   ├── AdvancedModelsDialog.tsx
│   │   │   │   ├── SaveToNotebooksDialog.tsx
│   │   │   │   └── StreamingResponse.tsx
│   │   │   ├── settings/
│   │   │   │   ├── EmbeddingModelChangeDialog.tsx
│   │   │   │   ├── MigrationBanner.tsx
│   │   │   │   ├── ModelTestResultDialog.tsx
│   │   │   │   └── index.ts
│   │   │   ├── source/
│   │   │   │   ├── ChatPanel.tsx
│   │   │   │   ├── MessageActions.tsx
│   │   │   │   ├── ModelSelector.tsx
│   │   │   │   ├── NotebookAssociations.tsx
│   │   │   │   ├── SessionManager.tsx
│   │   │   │   ├── SourceDetailContent.tsx
│   │   │   │   ├── SourceDialog.tsx
│   │   │   │   └── SourceInsightDialog.tsx
│   │   │   ├── sources/
│   │   │   │   ├── AddExistingSourceDialog.tsx
│   │   │   │   ├── AddSourceButton.tsx
│   │   │   │   ├── AddSourceDialog.tsx
│   │   │   │   ├── README.md
│   │   │   │   ├── SourceCard.tsx
│   │   │   │   ├── index.ts
│   │   │   │   └── steps/
│   │   │   │       ├── NotebooksStep.tsx
│   │   │   │       ├── ProcessingStep.tsx
│   │   │   │       └── SourceTypeStep.tsx
│   │   │   └── ui/
│   │   │       ├── CLAUDE.md
│   │   │       ├── accordion.tsx
│   │   │       ├── alert-dialog.tsx
│   │   │       ├── alert.tsx
│   │   │       ├── badge.tsx
│   │   │       ├── button.tsx
│   │   │       ├── card.tsx
│   │   │       ├── checkbox-list.tsx
│   │   │       ├── checkbox.tsx
│   │   │       ├── collapsible.tsx
│   │   │       ├── command.tsx
│   │   │       ├── dialog.tsx
│   │   │       ├── dropdown-menu.tsx
│   │   │       ├── form-section.tsx
│   │   │       ├── input.tsx
│   │   │       ├── label.tsx
│   │   │       ├── markdown-editor.tsx
│   │   │       ├── popover.tsx
│   │   │       ├── progress.tsx
│   │   │       ├── radio-group.tsx
│   │   │       ├── scroll-area.tsx
│   │   │       ├── select.tsx
│   │   │       ├── separator.tsx
│   │   │       ├── sonner.tsx
│   │   │       ├── tabs.tsx
│   │   │       ├── textarea.tsx
│   │   │       ├── tooltip.tsx
│   │   │       └── wizard-container.tsx
│   │   ├── lib/
│   │   │   ├── api/
│   │   │   │   ├── CLAUDE.md
│   │   │   │   ├── chat.ts
│   │   │   │   ├── client.ts
│   │   │   │   ├── credentials.ts
│   │   │   │   ├── embedding.ts
│   │   │   │   ├── insights.ts
│   │   │   │   ├── models.ts
│   │   │   │   ├── notebooks.ts
│   │   │   │   ├── notes.ts
│   │   │   │   ├── podcasts.ts
│   │   │   │   ├── query-client.ts
│   │   │   │   ├── search.ts
│   │   │   │   ├── settings.ts
│   │   │   │   ├── source-chat.ts
│   │   │   │   ├── sources.ts
│   │   │   │   └── transformations.ts
│   │   │   ├── config.test.ts
│   │   │   ├── config.ts
│   │   │   ├── hooks/
│   │   │   │   ├── CLAUDE.md
│   │   │   │   ├── use-ask.ts
│   │   │   │   ├── use-auth.ts
│   │   │   │   ├── use-create-dialogs.tsx
│   │   │   │   ├── use-credentials.ts
│   │   │   │   ├── use-insights.ts
│   │   │   │   ├── use-media-query.ts
│   │   │   │   ├── use-modal-manager.test.ts
│   │   │   │   ├── use-modal-manager.ts
│   │   │   │   ├── use-models.ts
│   │   │   │   ├── use-navigation.ts
│   │   │   │   ├── use-notebooks.ts
│   │   │   │   ├── use-notes.ts
│   │   │   │   ├── use-podcasts.ts
│   │   │   │   ├── use-search.ts
│   │   │   │   ├── use-settings.ts
│   │   │   │   ├── use-sources.ts
│   │   │   │   ├── use-toast.ts
│   │   │   │   ├── use-transformations.ts
│   │   │   │   ├── use-translation.test.ts
│   │   │   │   ├── use-translation.ts
│   │   │   │   ├── use-version-check.ts
│   │   │   │   ├── useNotebookChat.ts
│   │   │   │   └── useSourceChat.ts
│   │   │   ├── i18n-events.ts
│   │   │   ├── i18n.ts
│   │   │   ├── locales/
│   │   │   │   ├── CLAUDE.md
│   │   │   │   ├── bn-IN/
│   │   │   │   │   └── index.ts
│   │   │   │   ├── en-US/
│   │   │   │   │   └── index.ts
│   │   │   │   ├── fr-FR/
│   │   │   │   │   └── index.ts
│   │   │   │   ├── index.test.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── it-IT/
│   │   │   │   │   └── index.ts
│   │   │   │   ├── ja-JP/
│   │   │   │   │   └── index.ts
│   │   │   │   ├── pt-BR/
│   │   │   │   │   └── index.ts
│   │   │   │   ├── ru-RU/
│   │   │   │   │   └── index.ts
│   │   │   │   ├── zh-CN/
│   │   │   │   │   └── index.ts
│   │   │   │   └── zh-TW/
│   │   │   │       └── index.ts
│   │   │   ├── stores/
│   │   │   │   ├── CLAUDE.md
│   │   │   │   ├── auth-store.ts
│   │   │   │   ├── navigation-store.ts
│   │   │   │   ├── notebook-columns-store.ts
│   │   │   │   ├── sidebar-store.ts
│   │   │   │   └── theme-store.ts
│   │   │   ├── theme-script.ts
│   │   │   ├── types/
│   │   │   │   ├── api.ts
│   │   │   │   ├── auth.ts
│   │   │   │   ├── common.ts
│   │   │   │   ├── config.ts
│   │   │   │   ├── models.ts
│   │   │   │   ├── podcasts.ts
│   │   │   │   ├── search.ts
│   │   │   │   └── transformations.ts
│   │   │   ├── utils/
│   │   │   │   ├── date-locale.ts
│   │   │   │   ├── error-handler.ts
│   │   │   │   └── source-references.tsx
│   │   │   └── utils.ts
│   │   ├── proxy.ts
│   │   └── test/
│   │       ├── jest-dom.d.ts
│   │       └── setup.ts
│   ├── start-server.js
│   ├── tailwind.config.ts
│   ├── tsconfig.json
│   └── vitest.config.ts
├── mypy.ini
├── open_notebook/
│   ├── CLAUDE.md
│   ├── __init__.py
│   ├── ai/
│   │   ├── CLAUDE.md
│   │   ├── __init__.py
│   │   ├── connection_tester.py
│   │   ├── key_provider.py
│   │   ├── model_discovery.py
│   │   ├── models.py
│   │   └── provision.py
│   ├── config.py
│   ├── database/
│   │   ├── CLAUDE.md
│   │   ├── async_migrate.py
│   │   ├── migrate.py
│   │   ├── migrations/
│   │   │   ├── 1.surrealql
│   │   │   ├── 10.surrealql
│   │   │   ├── 10_down.surrealql
│   │   │   ├── 11.surrealql
│   │   │   ├── 11_down.surrealql
│   │   │   ├── 12.surrealql
│   │   │   ├── 12_down.surrealql
│   │   │   ├── 13.surrealql
│   │   │   ├── 13_down.surrealql
│   │   │   ├── 14.surrealql
│   │   │   ├── 14_down.surrealql
│   │   │   ├── 1_down.surrealql
│   │   │   ├── 2.surrealql
│   │   │   ├── 2_down.surrealql
│   │   │   ├── 3.surrealql
│   │   │   ├── 3_down.surrealql
│   │   │   ├── 4.surrealql
│   │   │   ├── 4_down.surrealql
│   │   │   ├── 5.surrealql
│   │   │   ├── 5_down.surrealql
│   │   │   ├── 6.surrealql
│   │   │   ├── 6_down.surrealql
│   │   │   ├── 7.surrealql
│   │   │   ├── 7_down.surrealql
│   │   │   ├── 8.surrealql
│   │   │   ├── 8_down.surrealql
│   │   │   ├── 9.surrealql
│   │   │   └── 9_down.surrealql
│   │   └── repository.py
│   ├── domain/
│   │   ├── CLAUDE.md
│   │   ├── __init__.py
│   │   ├── base.py
│   │   ├── content_settings.py
│   │   ├── credential.py
│   │   ├── notebook.py
│   │   ├── provider_config.py
│   │   └── transformation.py
│   ├── exceptions.py
│   ├── graphs/
│   │   ├── CLAUDE.md
│   │   ├── ask.py
│   │   ├── chat.py
│   │   ├── prompt.py
│   │   ├── source.py
│   │   ├── source_chat.py
│   │   ├── tools.py
│   │   └── transformation.py
│   ├── podcasts/
│   │   ├── CLAUDE.md
│   │   ├── __init__.py
│   │   ├── migration.py
│   │   └── models.py
│   └── utils/
│       ├── CLAUDE.md
│       ├── README.md
│       ├── __init__.py
│       ├── chunking.py
│       ├── context_builder.py
│       ├── embedding.py
│       ├── encryption.py
│       ├── error_classifier.py
│       ├── graph_utils.py
│       ├── text_utils.py
│       ├── token_utils.py
│       └── version_utils.py
├── prompts/
│   ├── CLAUDE.md
│   ├── ask/
│   │   ├── entry.jinja
│   │   ├── final_answer.jinja
│   │   └── query_process.jinja
│   ├── chat/
│   │   └── system.jinja
│   ├── podcast/
│   │   ├── outline.jinja
│   │   └── transcript.jinja
│   └── source_chat/
│       └── system.jinja
├── pyproject.toml
├── run_api.py
├── scripts/
│   ├── README.md
│   ├── export_docs.py
│   └── wait-for-api.sh
├── supervisord.conf
├── supervisord.single.conf
└── tests/
    ├── README.md
    ├── conftest.py
    ├── test_chunking.py
    ├── test_domain.py
    ├── test_embedding.py
    ├── test_graphs.py
    ├── test_models_api.py
    ├── test_notes_api.py
    ├── test_podcast_path.py
    ├── test_url_validation.py
    └── test_utils.py
Download .txt
SYMBOL INDEX (1452 symbols across 246 files)

FILE: api/auth.py
  class PasswordAuthMiddleware (line 12) | class PasswordAuthMiddleware(BaseHTTPMiddleware):
    method __init__ (line 19) | def __init__(self, app, excluded_paths: Optional[list] = None):
    method dispatch (line 30) | async def dispatch(self, request: Request, call_next):
  function check_api_password (line 82) | def check_api_password(

FILE: api/chat_service.py
  class ChatService (line 13) | class ChatService:
    method __init__ (line 16) | def __init__(self):
    method get_sessions (line 24) | async def get_sessions(self, notebook_id: str) -> List[Dict[str, Any]]:
    method create_session (line 39) | async def create_session(
    method get_session (line 65) | async def get_session(self, session_id: str) -> Dict[str, Any]:
    method update_session (line 79) | async def update_session(
    method delete_session (line 110) | async def delete_session(self, session_id: str) -> Dict[str, Any]:
    method execute_chat (line 124) | async def execute_chat(
    method build_context (line 149) | async def build_context(

FILE: api/client.py
  class APIClient (line 13) | class APIClient:
    method __init__ (line 16) | def __init__(self, base_url: Optional[str] = None):
    method _make_request (line 48) | def _make_request(
    method get_notebooks (line 80) | def get_notebooks(
    method create_notebook (line 91) | def create_notebook(
    method get_notebook (line 98) | def get_notebook(
    method update_notebook (line 104) | def update_notebook(
    method delete_notebook (line 110) | def delete_notebook(
    method search (line 117) | def search(
    method ask_simple (line 137) | def ask_simple(
    method get_models (line 157) | def get_models(self, model_type: Optional[str] = None) -> List[Dict[An...
    method create_model (line 165) | def create_model(
    method delete_model (line 176) | def delete_model(
    method get_default_models (line 182) | def get_default_models(self) -> Union[Dict[Any, Any], List[Dict[Any, A...
    method update_default_models (line 186) | def update_default_models(
    method get_transformations (line 193) | def get_transformations(self) -> List[Dict[Any, Any]]:
    method create_transformation (line 198) | def create_transformation(
    method get_transformation (line 216) | def get_transformation(
    method update_transformation (line 222) | def update_transformation(
    method delete_transformation (line 230) | def delete_transformation(
    method execute_transformation (line 236) | def execute_transformation(
    method get_notes (line 251) | def get_notes(self, notebook_id: Optional[str] = None) -> List[Dict[An...
    method create_note (line 259) | def create_note(
    method get_note (line 277) | def get_note(self, note_id: str) -> Union[Dict[Any, Any], List[Dict[An...
    method update_note (line 281) | def update_note(
    method delete_note (line 287) | def delete_note(self, note_id: str) -> Union[Dict[Any, Any], List[Dict...
    method embed_content (line 292) | def embed_content(
    method rebuild_embeddings (line 304) | def rebuild_embeddings(
    method get_rebuild_status (line 328) | def get_rebuild_status(
    method get_settings (line 335) | def get_settings(self) -> Union[Dict[Any, Any], List[Dict[Any, Any]]]:
    method update_settings (line 339) | def update_settings(
    method get_notebook_context (line 346) | def get_notebook_context(
    method get_sources (line 359) | def get_sources(self, notebook_id: Optional[str] = None) -> List[Dict[...
    method create_source (line 367) | def create_source(
    method get_source (line 413) | def get_source(self, source_id: str) -> Union[Dict[Any, Any], List[Dic...
    method get_source_status (line 417) | def get_source_status(
    method update_source (line 423) | def update_source(
    method delete_source (line 429) | def delete_source(
    method get_source_insights (line 436) | def get_source_insights(self, source_id: str) -> List[Dict[Any, Any]]:
    method get_insight (line 441) | def get_insight(
    method delete_insight (line 447) | def delete_insight(
    method save_insight_as_note (line 453) | def save_insight_as_note(
    method create_source_insight (line 464) | def create_source_insight(
    method get_episode_profiles (line 476) | def get_episode_profiles(self) -> List[Dict[Any, Any]]:
    method get_episode_profile (line 481) | def get_episode_profile(
    method create_episode_profile (line 487) | def create_episode_profile(
    method update_episode_profile (line 513) | def update_episode_profile(
    method delete_episode_profile (line 521) | def delete_episode_profile(

FILE: api/command_service.py
  class CommandService (line 7) | class CommandService:
    method submit_command_job (line 11) | async def submit_command_job(
    method get_command_status (line 47) | async def get_command_status(job_id: str) -> Dict[str, Any]:
    method list_command_jobs (line 71) | async def list_command_jobs(
    method cancel_command_job (line 83) | async def cancel_command_job(job_id: str) -> bool:

FILE: api/context_service.py
  class ContextService (line 12) | class ContextService:
    method __init__ (line 15) | def __init__(self):
    method get_notebook_context (line 18) | def get_notebook_context(

FILE: api/credentials_service.py
  function validate_url (line 85) | def validate_url(url: str, provider: str) -> None:
  function require_encryption_key (line 192) | def require_encryption_key() -> None:
  function credential_to_response (line 201) | def credential_to_response(cred: Credential, model_count: int = 0) -> Cr...
  function check_env_configured (line 225) | def check_env_configured(provider: str) -> bool:
  function get_default_modalities (line 238) | def get_default_modalities(provider: str) -> List[str]:
  function create_credential_from_env (line 243) | def create_credential_from_env(provider: str) -> Credential:
  function get_provider_status (line 314) | async def get_provider_status() -> dict:
  function get_env_status (line 348) | async def get_env_status() -> Dict[str, bool]:
  function test_credential (line 356) | async def test_credential(credential_id: str) -> dict:
  function discover_with_config (line 468) | async def discover_with_config(provider: str, config: dict) -> List[dict]:
  function register_models (line 641) | async def register_models(credential_id: str, models_data: list) -> dict:
  function migrate_from_provider_config (line 686) | async def migrate_from_provider_config() -> dict:
  function migrate_from_env (line 794) | async def migrate_from_env() -> dict:

FILE: api/embedding_service.py
  class EmbeddingService (line 12) | class EmbeddingService:
    method __init__ (line 15) | def __init__(self):
    method embed_content (line 18) | def embed_content(

FILE: api/episode_profiles_service.py
  class EpisodeProfilesService (line 13) | class EpisodeProfilesService:
    method __init__ (line 16) | def __init__(self):
    method get_all_episode_profiles (line 19) | def get_all_episode_profiles(self) -> List[EpisodeProfile]:
    method get_episode_profile (line 40) | def get_episode_profile(self, profile_name: str) -> EpisodeProfile:
    method create_episode_profile (line 62) | def create_episode_profile(
    method delete_episode_profile (line 105) | def delete_episode_profile(self, profile_id: str) -> bool:

FILE: api/insights_service.py
  class InsightsService (line 13) | class InsightsService:
    method __init__ (line 16) | def __init__(self):
    method get_source_insights (line 19) | def get_source_insights(self, source_id: str) -> List[SourceInsight]:
    method get_insight (line 35) | def get_insight(self, insight_id: str) -> SourceInsight:
    method delete_insight (line 53) | def delete_insight(self, insight_id: str) -> bool:
    method save_insight_as_note (line 58) | def save_insight_as_note(
    method create_source_insight (line 76) | def create_source_insight(

FILE: api/main.py
  function lifespan (line 59) | async def lifespan(app: FastAPI):
  function custom_http_exception_handler (line 153) | async def custom_http_exception_handler(request: Request, exc: Starlette...
  function _cors_headers (line 177) | def _cors_headers(request: Request) -> dict[str, str]:
  function not_found_error_handler (line 188) | async def not_found_error_handler(request: Request, exc: NotFoundError):
  function invalid_input_error_handler (line 197) | async def invalid_input_error_handler(request: Request, exc: InvalidInpu...
  function authentication_error_handler (line 206) | async def authentication_error_handler(request: Request, exc: Authentica...
  function rate_limit_error_handler (line 215) | async def rate_limit_error_handler(request: Request, exc: RateLimitError):
  function configuration_error_handler (line 224) | async def configuration_error_handler(request: Request, exc: Configurati...
  function network_error_handler (line 233) | async def network_error_handler(request: Request, exc: NetworkError):
  function external_service_error_handler (line 242) | async def external_service_error_handler(request: Request, exc: External...
  function open_notebook_error_handler (line 251) | async def open_notebook_error_handler(request: Request, exc: OpenNoteboo...
  function root (line 286) | async def root():
  function health (line 291) | async def health():

FILE: api/models.py
  class NotebookCreate (line 7) | class NotebookCreate(BaseModel):
  class NotebookUpdate (line 12) | class NotebookUpdate(BaseModel):
  class NotebookResponse (line 20) | class NotebookResponse(BaseModel):
  class SearchRequest (line 32) | class SearchRequest(BaseModel):
  class SearchResponse (line 43) | class SearchResponse(BaseModel):
  class AskRequest (line 49) | class AskRequest(BaseModel):
  class AskResponse (line 56) | class AskResponse(BaseModel):
  class ModelCreate (line 62) | class ModelCreate(BaseModel):
  class ModelResponse (line 76) | class ModelResponse(BaseModel):
  class DefaultModelsResponse (line 86) | class DefaultModelsResponse(BaseModel):
  class ProviderAvailabilityResponse (line 96) | class ProviderAvailabilityResponse(BaseModel):
  class TransformationCreate (line 105) | class TransformationCreate(BaseModel):
  class TransformationUpdate (line 117) | class TransformationUpdate(BaseModel):
  class TransformationResponse (line 131) | class TransformationResponse(BaseModel):
  class TransformationExecuteRequest (line 142) | class TransformationExecuteRequest(BaseModel):
  class TransformationExecuteResponse (line 152) | class TransformationExecuteResponse(BaseModel):
  class DefaultPromptResponse (line 161) | class DefaultPromptResponse(BaseModel):
  class DefaultPromptUpdate (line 167) | class DefaultPromptUpdate(BaseModel):
  class NoteCreate (line 174) | class NoteCreate(BaseModel):
  class NoteUpdate (line 183) | class NoteUpdate(BaseModel):
  class NoteResponse (line 189) | class NoteResponse(BaseModel):
  class EmbedRequest (line 200) | class EmbedRequest(BaseModel):
  class EmbedResponse (line 208) | class EmbedResponse(BaseModel):
  class RebuildRequest (line 219) | class RebuildRequest(BaseModel):
  class RebuildResponse (line 229) | class RebuildResponse(BaseModel):
  class RebuildProgress (line 235) | class RebuildProgress(BaseModel):
  class RebuildStats (line 241) | class RebuildStats(BaseModel):
  class RebuildStatusResponse (line 248) | class RebuildStatusResponse(BaseModel):
  class SettingsResponse (line 259) | class SettingsResponse(BaseModel):
  class SettingsUpdate (line 267) | class SettingsUpdate(BaseModel):
  class AssetModel (line 276) | class AssetModel(BaseModel):
  class SourceCreate (line 281) | class SourceCreate(BaseModel):
    method validate_notebook_fields (line 309) | def validate_notebook_fields(self):
  class SourceUpdate (line 328) | class SourceUpdate(BaseModel):
  class SourceResponse (line 333) | class SourceResponse(BaseModel):
  class SourceListResponse (line 352) | class SourceListResponse(BaseModel):
  class ContextConfig (line 370) | class ContextConfig(BaseModel):
  class ContextRequest (line 379) | class ContextRequest(BaseModel):
  class ContextResponse (line 386) | class ContextResponse(BaseModel):
  class SourceInsightResponse (line 394) | class SourceInsightResponse(BaseModel):
  class InsightCreationResponse (line 403) | class InsightCreationResponse(BaseModel):
  class SaveAsNoteRequest (line 413) | class SaveAsNoteRequest(BaseModel):
  class CreateSourceInsightRequest (line 417) | class CreateSourceInsightRequest(BaseModel):
  class SourceStatusResponse (line 427) | class SourceStatusResponse(BaseModel):
  class ErrorResponse (line 437) | class ErrorResponse(BaseModel):
  class SetApiKeyRequest (line 443) | class SetApiKeyRequest(BaseModel):
    method validate_not_empty_string (line 498) | def validate_not_empty_string(cls, v: Optional[str]) -> Optional[str]:
  class ApiKeyStatusResponse (line 508) | class ApiKeyStatusResponse(BaseModel):
  class TestConnectionResponse (line 524) | class TestConnectionResponse(BaseModel):
  class MigrateFromEnvRequest (line 532) | class MigrateFromEnvRequest(BaseModel):
  class MigrationResult (line 540) | class MigrationResult(BaseModel):
  class CreateCredentialRequest (line 557) | class CreateCredentialRequest(BaseModel):
  class UpdateCredentialRequest (line 581) | class UpdateCredentialRequest(BaseModel):
  class CredentialResponse (line 599) | class CredentialResponse(BaseModel):
  class CredentialDeleteResponse (line 622) | class CredentialDeleteResponse(BaseModel):
  class DiscoveredModelResponse (line 629) | class DiscoveredModelResponse(BaseModel):
  class DiscoverModelsResponse (line 638) | class DiscoverModelsResponse(BaseModel):
  class RegisterModelData (line 646) | class RegisterModelData(BaseModel):
  class RegisterModelsRequest (line 654) | class RegisterModelsRequest(BaseModel):
  class RegisterModelsResponse (line 660) | class RegisterModelsResponse(BaseModel):
  class NotebookDeletePreview (line 667) | class NotebookDeletePreview(BaseModel):
  class NotebookDeleteResponse (line 679) | class NotebookDeleteResponse(BaseModel):

FILE: api/models_service.py
  class ModelsService (line 13) | class ModelsService:
    method __init__ (line 16) | def __init__(self):
    method get_all_models (line 19) | def get_all_models(self, model_type: Optional[str] = None) -> List[Mod...
    method create_model (line 36) | def create_model(self, name: str, provider: str, model_type: str) -> M...
    method delete_model (line 50) | def delete_model(self, model_id: str) -> bool:
    method get_default_models (line 55) | def get_default_models(self) -> DefaultModels:
    method update_default_models (line 78) | def update_default_models(self, defaults: DefaultModels) -> DefaultMod...

FILE: api/notebook_service.py
  class NotebookService (line 13) | class NotebookService:
    method __init__ (line 16) | def __init__(self):
    method get_all_notebooks (line 19) | def get_all_notebooks(self, order_by: str = "updated desc") -> List[No...
    method get_notebook (line 36) | def get_notebook(self, notebook_id: str) -> Optional[Notebook]:
    method create_notebook (line 50) | def create_notebook(self, name: str, description: str = "") -> Notebook:
    method update_notebook (line 64) | def update_notebook(self, notebook: Notebook) -> Notebook:
    method delete_notebook (line 80) | def delete_notebook(self, notebook: Notebook) -> bool:

FILE: api/notes_service.py
  class NotesService (line 13) | class NotesService:
    method __init__ (line 16) | def __init__(self):
    method get_all_notes (line 19) | def get_all_notes(self, notebook_id: Optional[str] = None) -> List[Note]:
    method get_note (line 36) | def get_note(self, note_id: str) -> Note:
    method create_note (line 52) | def create_note(
    method update_note (line 76) | def update_note(self, note: Note) -> Note:
    method delete_note (line 96) | def delete_note(self, note_id: str) -> bool:

FILE: api/podcast_api_service.py
  class PodcastAPIService (line 13) | class PodcastAPIService:
    method __init__ (line 16) | def __init__(self):
    method get_episodes (line 20) | def get_episodes(self) -> List[Dict[Any, Any]]:
    method delete_episode (line 25) | def delete_episode(self, episode_id: str) -> bool:
    method get_episode_profiles (line 35) | def get_episode_profiles(self) -> List[Dict]:
    method create_episode_profile (line 39) | def create_episode_profile(self, profile_data: Dict) -> bool:
    method update_episode_profile (line 48) | def update_episode_profile(self, profile_id: str, profile_data: Dict) ...
    method delete_episode_profile (line 57) | def delete_episode_profile(self, profile_id: str) -> bool:
    method duplicate_episode_profile (line 66) | def duplicate_episode_profile(self, profile_id: str) -> bool:
    method get_speaker_profiles (line 78) | def get_speaker_profiles(self) -> List[Dict[Any, Any]]:
    method create_speaker_profile (line 83) | def create_speaker_profile(self, profile_data: Dict) -> bool:
    method update_speaker_profile (line 92) | def update_speaker_profile(self, profile_id: str, profile_data: Dict) ...
    method delete_speaker_profile (line 103) | def delete_speaker_profile(self, profile_id: str) -> bool:
    method duplicate_speaker_profile (line 112) | def duplicate_speaker_profile(self, profile_id: str) -> bool:

FILE: api/podcast_service.py
  class PodcastGenerationRequest (line 12) | class PodcastGenerationRequest(BaseModel):
  class PodcastGenerationResponse (line 23) | class PodcastGenerationResponse(BaseModel):
  class PodcastService (line 33) | class PodcastService:
    method submit_generation_job (line 37) | async def submit_generation_job(
    method get_job_status (line 115) | async def get_job_status(job_id: str) -> Dict[str, Any]:
    method list_episodes (line 141) | async def list_episodes() -> list:
    method get_episode (line 153) | async def get_episode(episode_id: str) -> PodcastEpisode:
  class DefaultProfiles (line 163) | class DefaultProfiles:
    method create_default_episode_profiles (line 167) | async def create_default_episode_profiles():
    method create_default_speaker_profiles (line 188) | async def create_default_speaker_profiles():

FILE: api/routers/auth.py
  function get_auth_status (line 14) | async def get_auth_status():

FILE: api/routers/chat.py
  class CreateSessionRequest (line 22) | class CreateSessionRequest(BaseModel):
  class UpdateSessionRequest (line 30) | class UpdateSessionRequest(BaseModel):
  class ChatMessage (line 37) | class ChatMessage(BaseModel):
  class ChatSessionResponse (line 44) | class ChatSessionResponse(BaseModel):
  class ChatSessionWithMessagesResponse (line 58) | class ChatSessionWithMessagesResponse(ChatSessionResponse):
  class ExecuteChatRequest (line 64) | class ExecuteChatRequest(BaseModel):
  class ExecuteChatResponse (line 75) | class ExecuteChatResponse(BaseModel):
  class BuildContextRequest (line 80) | class BuildContextRequest(BaseModel):
  class BuildContextResponse (line 85) | class BuildContextResponse(BaseModel):
  class SuccessResponse (line 91) | class SuccessResponse(BaseModel):
  function get_sessions (line 97) | async def get_sessions(notebook_id: str = Query(..., description="Notebo...
  function create_session (line 138) | async def create_session(request: CreateSessionRequest):
  function get_session (line 178) | async def get_session(session_id: str):
  function update_session (line 251) | async def update_session(session_id: str, request: UpdateSessionRequest):
  function delete_session (line 307) | async def delete_session(session_id: str):
  function execute_chat (line 331) | async def execute_chat(request: ExecuteChatRequest):
  function build_context (line 412) | async def build_context(request: BuildContextRequest):

FILE: api/routers/commands.py
  class CommandExecutionRequest (line 13) | class CommandExecutionRequest(BaseModel):
  class CommandJobResponse (line 21) | class CommandJobResponse(BaseModel):
  class CommandJobStatusResponse (line 27) | class CommandJobStatusResponse(BaseModel):
  function execute_command (line 38) | async def execute_command(request: CommandExecutionRequest):
  function get_command_job_status (line 75) | async def get_command_job_status(job_id: str):
  function list_command_jobs (line 89) | async def list_command_jobs(
  function cancel_command_job (line 109) | async def cancel_command_job(job_id: str):
  function debug_registry (line 123) | async def debug_registry():

FILE: api/routers/config.py
  function get_version (line 31) | def get_version() -> str:
  function get_latest_version_cached (line 43) | async def get_latest_version_cached(current_version: str) -> tuple[Optio...
  function check_database_health (line 102) | async def check_database_health() -> dict:
  function get_config (line 124) | async def get_config(request: Request):

FILE: api/routers/context.py
  function get_notebook_context (line 13) | async def get_notebook_context(notebook_id: str, context_request: Contex...

FILE: api/routers/credentials.py
  function _handle_value_error (line 56) | def _handle_value_error(e: ValueError, status_code: int = 400) -> HTTPEx...
  function get_status (line 67) | async def get_status():
  function get_env_status (line 80) | async def get_env_status():
  function list_credentials (line 95) | async def list_credentials(
  function list_credentials_by_provider (line 118) | async def list_credentials_by_provider(provider: str):
  function create_credential (line 133) | async def create_credential(request: CreateCredentialRequest):
  function get_credential (line 177) | async def get_credential(credential_id: str):
  function update_credential (line 189) | async def update_credential(credential_id: str, request: UpdateCredentia...
  function delete_credential (line 249) | async def delete_credential(
  function test_credential (line 314) | async def test_credential(credential_id: str):
  function discover_models_for_credential (line 320) | async def discover_models_for_credential(credential_id: str):
  function register_models_for_credential (line 348) | async def register_models_for_credential(
  function migrate_from_provider_config (line 366) | async def migrate_from_provider_config():
  function migrate_from_env (line 378) | async def migrate_from_env():

FILE: api/routers/embedding.py
  function embed_content (line 13) | async def embed_content(embed_request: EmbedRequest):

FILE: api/routers/embedding_rebuild.py
  function start_rebuild (line 19) | async def start_rebuild(request: RebuildRequest):
  function get_rebuild_status (line 124) | async def get_rebuild_status(command_id: str):

FILE: api/routers/episode_profiles.py
  class EpisodeProfileResponse (line 12) | class EpisodeProfileResponse(BaseModel):
  function _profile_to_response (line 29) | def _profile_to_response(profile: EpisodeProfile) -> EpisodeProfileRespo...
  function list_episode_profiles (line 48) | async def list_episode_profiles():
  function get_episode_profile (line 61) | async def get_episode_profile(profile_name: str):
  class EpisodeProfileCreate (line 82) | class EpisodeProfileCreate(BaseModel):
  function create_episode_profile (line 101) | async def create_episode_profile(profile_data: EpisodeProfileCreate):
  function update_episode_profile (line 130) | async def update_episode_profile(profile_id: str, profile_data: EpisodeP...
  function delete_episode_profile (line 166) | async def delete_episode_profile(profile_id: str):
  function duplicate_episode_profile (line 192) | async def duplicate_episode_profile(profile_id: str):

FILE: api/routers/insights.py
  function get_insight (line 12) | async def get_insight(insight_id: str):
  function delete_insight (line 38) | async def delete_insight(insight_id: str):
  function save_insight_as_note (line 56) | async def save_insight_as_note(insight_id: str, request: SaveAsNoteReque...

FILE: api/routers/languages.py
  class LanguageResponse (line 32) | class LanguageResponse(BaseModel):
  function list_languages (line 38) | async def list_languages():

FILE: api/routers/models.py
  class DiscoveredModelResponse (line 36) | class DiscoveredModelResponse(BaseModel):
  class ProviderSyncResponse (line 45) | class ProviderSyncResponse(BaseModel):
  class AllProvidersSyncResponse (line 54) | class AllProvidersSyncResponse(BaseModel):
  class ProviderModelCountResponse (line 62) | class ProviderModelCountResponse(BaseModel):
  class AutoAssignResult (line 70) | class AutoAssignResult(BaseModel):
  class ModelTestResponse (line 78) | class ModelTestResponse(BaseModel):
  function _check_provider_has_credential (line 111) | async def _check_provider_has_credential(provider: str) -> bool:
  function _check_azure_support (line 121) | def _check_azure_support(mode: str) -> bool:
  function _check_openai_compatible_support (line 148) | def _check_openai_compatible_support(mode: str) -> bool:
  function get_models (line 166) | async def get_models(
  function create_model (line 194) | async def create_model(model_data: ModelCreate):
  function delete_model (line 249) | async def delete_model(model_id: str):
  function test_model (line 267) | async def test_model(model_id: str):
  function get_default_models (line 290) | async def get_default_models():
  function update_default_models (line 312) | async def update_default_models(defaults_data: DefaultModelsResponse):
  function get_provider_availability (line 362) | async def get_provider_availability():
  function discover_models (line 484) | async def discover_models(provider: str):
  function sync_models (line 513) | async def sync_models(provider: str):
  function sync_all_models (line 540) | async def sync_all_models():
  function get_model_count (line 578) | async def get_model_count(provider: str):
  function get_models_by_provider (line 601) | async def get_models_by_provider(provider: str):
  function _get_preferred_model (line 634) | def _get_preferred_model(
  function auto_assign_defaults (line 679) | async def auto_assign_defaults():

FILE: api/routers/notebooks.py
  function get_notebooks (line 21) | async def get_notebooks(
  function create_notebook (line 63) | async def create_notebook(notebook: NotebookCreate):
  function get_notebook_delete_preview (line 94) | async def get_notebook_delete_preview(notebook_id: str):
  function get_notebook (line 121) | async def get_notebook(notebook_id: str):
  function update_notebook (line 157) | async def update_notebook(notebook_id: str, notebook_update: NotebookUpd...
  function add_source_to_notebook (line 219) | async def add_source_to_notebook(notebook_id: str, source_id: str):
  function remove_source_from_notebook (line 264) | async def remove_source_from_notebook(notebook_id: str, source_id: str):
  function delete_notebook (line 294) | async def delete_notebook(

FILE: api/routers/notes.py
  function get_notes (line 14) | async def get_notes(
  function create_note (line 50) | async def create_note(note_data: NoteCreate):
  function get_note (line 111) | async def get_note(note_id: str):
  function update_note (line 134) | async def update_note(note_id: str, note_update: NoteUpdate):
  function delete_note (line 175) | async def delete_note(note_id: str):

FILE: api/routers/podcasts.py
  class PodcastEpisodeResponse (line 19) | class PodcastEpisodeResponse(BaseModel):
  function _resolve_audio_path (line 34) | def _resolve_audio_path(audio_file: str) -> Path:
  function generate_podcast (line 42) | async def generate_podcast(request: PodcastGenerationRequest):
  function get_podcast_job_status (line 73) | async def get_podcast_job_status(job_id: str):
  function list_podcast_episodes (line 87) | async def list_podcast_episodes():
  function get_podcast_episode (line 145) | async def get_podcast_episode(episode_id: str):
  function stream_podcast_episode_audio (line 191) | async def stream_podcast_episode_audio(episode_id: str):
  function retry_podcast_episode (line 216) | async def retry_podcast_episode(episode_id: str):
  function delete_podcast_episode (line 273) | async def delete_podcast_episode(episode_id: str):

FILE: api/routers/search.py
  function search_knowledge_base (line 18) | async def search_knowledge_base(search_request: SearchRequest):
  function stream_ask_response (line 61) | async def stream_ask_response(
  function ask_knowledge_base (line 114) | async def ask_knowledge_base(ask_request: AskRequest):
  function ask_knowledge_base_simple (line 161) | async def ask_knowledge_base_simple(ask_request: AskRequest):

FILE: api/routers/settings.py
  function get_settings (line 12) | async def get_settings():
  function update_settings (line 32) | async def update_settings(settings_update: SettingsUpdate):

FILE: api/routers/source_chat.py
  class CreateSourceChatSessionRequest (line 24) | class CreateSourceChatSessionRequest(BaseModel):
  class UpdateSourceChatSessionRequest (line 31) | class UpdateSourceChatSessionRequest(BaseModel):
  class ChatMessage (line 37) | class ChatMessage(BaseModel):
  class ContextIndicator (line 44) | class ContextIndicator(BaseModel):
  class SourceChatSessionResponse (line 55) | class SourceChatSessionResponse(BaseModel):
  class SourceChatSessionWithMessagesResponse (line 68) | class SourceChatSessionWithMessagesResponse(SourceChatSessionResponse):
  class SendMessageRequest (line 76) | class SendMessageRequest(BaseModel):
  class SuccessResponse (line 82) | class SuccessResponse(BaseModel):
  function create_source_chat_session (line 90) | async def create_source_chat_session(
  function get_source_chat_sessions (line 135) | async def get_source_chat_sessions(source_id: str = Path(..., descriptio...
  function get_source_chat_session (line 195) | async def get_source_chat_session(
  function update_source_chat_session (line 292) | async def update_source_chat_session(
  function delete_source_chat_session (line 363) | async def delete_source_chat_session(
  function stream_source_chat_response (line 415) | async def stream_source_chat_response(
  function send_message_to_source_chat (line 482) | async def send_message_to_source_chat(

FILE: api/routers/sources.py
  function generate_unique_filename (line 41) | def generate_unique_filename(original_filename: str, upload_folder: str)...
  function save_uploaded_file (line 64) | async def save_uploaded_file(upload_file: UploadFile) -> str:
  function parse_source_form_data (line 88) | def parse_source_form_data(
  function get_sources (line 153) | async def get_sources(
  function create_source (line 281) | async def create_source(
  function create_source_json (line 555) | async def create_source_json(source_data: SourceCreate):
  function _resolve_source_file (line 562) | async def _resolve_source_file(source_id: str) -> tuple[str, str]:
  function _is_source_file_available (line 587) | def _is_source_file_available(source: Source) -> Optional[bool]:
  function get_source (line 602) | async def get_source(source_id: str):
  function check_source_file (line 662) | async def check_source_file(source_id: str):
  function download_source_file (line 675) | async def download_source_file(source_id: str):
  function get_source_status (line 692) | async def get_source_status(source_id: str):
  function update_source (line 754) | async def update_source(source_id: str, source_update: SourceUpdate):
  function retry_source_processing (line 796) | async def retry_source_processing(source_id: str):
  function delete_source (line 921) | async def delete_source(source_id: str):
  function get_source_insights (line 939) | async def get_source_insights(source_id: str):
  function create_source_insight (line 972) | async def create_source_insight(source_id: str, request: CreateSourceIns...

FILE: api/routers/speaker_profiles.py
  class SpeakerProfileResponse (line 12) | class SpeakerProfileResponse(BaseModel):
  function _profile_to_response (line 23) | def _profile_to_response(profile: SpeakerProfile) -> SpeakerProfileRespo...
  function list_speaker_profiles (line 36) | async def list_speaker_profiles():
  function get_speaker_profile (line 49) | async def get_speaker_profile(profile_name: str):
  class SpeakerProfileCreate (line 70) | class SpeakerProfileCreate(BaseModel):
  function create_speaker_profile (line 83) | async def create_speaker_profile(profile_data: SpeakerProfileCreate):
  function update_speaker_profile (line 106) | async def update_speaker_profile(profile_id: str, profile_data: SpeakerP...
  function delete_speaker_profile (line 136) | async def delete_speaker_profile(profile_id: str):
  function duplicate_speaker_profile (line 162) | async def duplicate_speaker_profile(profile_id: str):

FILE: api/routers/transformations.py
  function get_transformations (line 24) | async def get_transformations():
  function create_transformation (line 50) | async def create_transformation(transformation_data: TransformationCreate):
  function execute_transformation (line 82) | async def execute_transformation(execute_request: TransformationExecuteR...
  function get_default_prompt (line 122) | async def get_default_prompt():
  function update_default_prompt (line 139) | async def update_default_prompt(prompt_update: DefaultPromptUpdate):
  function get_transformation (line 162) | async def get_transformation(transformation_id: str):
  function update_transformation (line 191) | async def update_transformation(
  function delete_transformation (line 236) | async def delete_transformation(transformation_id: str):

FILE: api/search_service.py
  class SearchService (line 12) | class SearchService:
    method __init__ (line 15) | def __init__(self):
    method search (line 18) | def search(
    method ask_knowledge_base (line 40) | def ask_knowledge_base(

FILE: api/settings_service.py
  class SettingsService (line 11) | class SettingsService:
    method __init__ (line 14) | def __init__(self):
    method get_settings (line 17) | def get_settings(self) -> ContentSettings:
    method update_settings (line 43) | def update_settings(self, settings: ContentSettings) -> ContentSettings:

FILE: api/sources_service.py
  class SourceProcessingResult (line 15) | class SourceProcessingResult:
  class SourceWithMetadata (line 26) | class SourceWithMetadata:
    method id (line 34) | def id(self):
    method title (line 38) | def title(self):
    method title (line 42) | def title(self, value):
    method topics (line 46) | def topics(self):
    method asset (line 50) | def asset(self):
    method full_text (line 54) | def full_text(self):
    method created (line 58) | def created(self):
    method updated (line 62) | def updated(self):
  class SourcesService (line 66) | class SourcesService:
    method __init__ (line 69) | def __init__(self):
    method get_all_sources (line 72) | def get_all_sources(
    method get_source (line 103) | def get_source(self, source_id: str) -> SourceWithMetadata:
    method create_source (line 128) | def create_source(
    method get_source_status (line 219) | def get_source_status(self, source_id: str) -> Dict:
    method create_source_async (line 224) | def create_source_async(
    method is_source_processing_complete (line 267) | def is_source_processing_complete(self, source_id: str) -> bool:
    method update_source (line 286) | def update_source(self, source: Source) -> Source:
    method delete_source (line 309) | def delete_source(self, source_id: str) -> bool:

FILE: api/transformations_service.py
  class TransformationsService (line 14) | class TransformationsService:
    method __init__ (line 17) | def __init__(self):
    method get_all_transformations (line 20) | def get_all_transformations(self) -> List[Transformation]:
    method get_transformation (line 43) | def get_transformation(self, transformation_id: str) -> Transformation:
    method create_transformation (line 63) | def create_transformation(
    method update_transformation (line 96) | def update_transformation(self, transformation: Transformation) -> Tra...
    method delete_transformation (line 123) | def delete_transformation(self, transformation_id: str) -> bool:
    method execute_transformation (line 128) | def execute_transformation(

FILE: commands/embedding_commands.py
  function full_model_dump (line 16) | def full_model_dump(model):
  function get_command_id (line 27) | def get_command_id(input_data: CommandInput) -> str:
  class RebuildEmbeddingsInput (line 34) | class RebuildEmbeddingsInput(CommandInput):
  class RebuildEmbeddingsOutput (line 41) | class RebuildEmbeddingsOutput(CommandOutput):
  class CreateInsightInput (line 58) | class CreateInsightInput(CommandInput):
  class CreateInsightOutput (line 66) | class CreateInsightOutput(CommandOutput):
  class EmbedNoteInput (line 75) | class EmbedNoteInput(CommandInput):
  class EmbedNoteOutput (line 81) | class EmbedNoteOutput(CommandOutput):
  class EmbedInsightInput (line 90) | class EmbedInsightInput(CommandInput):
  class EmbedInsightOutput (line 96) | class EmbedInsightOutput(CommandOutput):
  class EmbedSourceInput (line 105) | class EmbedSourceInput(CommandInput):
  class EmbedSourceOutput (line 111) | class EmbedSourceOutput(CommandOutput):
  function embed_note_command (line 133) | async def embed_note_command(input_data: EmbedNoteInput) -> EmbedNoteOut...
  function embed_insight_command (line 225) | async def embed_insight_command(input_data: EmbedInsightInput) -> EmbedI...
  function embed_source_command (line 319) | async def embed_source_command(input_data: EmbedSourceInput) -> EmbedSou...
  function create_insight_command (line 455) | async def create_insight_command(
  function collect_items_for_rebuild (line 549) | async def collect_items_for_rebuild(
  function rebuild_embeddings_command (line 623) | async def rebuild_embeddings_command(

FILE: commands/example_commands.py
  class TextProcessingInput (line 10) | class TextProcessingInput(BaseModel):
  class TextProcessingOutput (line 16) | class TextProcessingOutput(BaseModel):
  class DataAnalysisInput (line 25) | class DataAnalysisInput(BaseModel):
  class DataAnalysisOutput (line 31) | class DataAnalysisOutput(BaseModel):
  function process_text_command (line 44) | async def process_text_command(input_data: TextProcessingInput) -> TextP...
  function analyze_data_command (line 95) | async def analyze_data_command(input_data: DataAnalysisInput) -> DataAna...

FILE: commands/podcast_commands.py
  function build_episode_output_dir (line 26) | def build_episode_output_dir(data_folder: str) -> tuple[str, Path]:
  function full_model_dump (line 40) | def full_model_dump(model):
  class PodcastGenerationInput (line 51) | class PodcastGenerationInput(CommandInput):
  class PodcastGenerationOutput (line 59) | class PodcastGenerationOutput(CommandOutput):
  function generate_podcast_command (line 70) | async def generate_podcast_command(

FILE: commands/source_commands.py
  function full_model_dump (line 21) | def full_model_dump(model):
  class SourceProcessingInput (line 32) | class SourceProcessingInput(CommandInput):
  class SourceProcessingOutput (line 40) | class SourceProcessingOutput(CommandOutput):
  function process_source_command (line 61) | async def process_source_command(
  class RunTransformationInput (line 164) | class RunTransformationInput(CommandInput):
  class RunTransformationOutput (line 171) | class RunTransformationOutput(CommandOutput):
  function run_transformation_command (line 193) | async def run_transformation_command(

FILE: frontend/next.config.ts
  method rewrites (line 18) | async rewrites() {

FILE: frontend/src/app/(auth)/login/page.tsx
  function LoginPage (line 4) | function LoginPage() {

FILE: frontend/src/app/(dashboard)/advanced/components/RebuildEmbeddings.tsx
  function RebuildEmbeddings (line 23) | function RebuildEmbeddings() {

FILE: frontend/src/app/(dashboard)/advanced/components/SystemInfo.tsx
  function SystemInfo (line 9) | function SystemInfo() {

FILE: frontend/src/app/(dashboard)/advanced/page.tsx
  function AdvancedPage (line 8) | function AdvancedPage() {

FILE: frontend/src/app/(dashboard)/layout.tsx
  function DashboardLayout (line 13) | function DashboardLayout({

FILE: frontend/src/app/(dashboard)/notebooks/[id]/page.tsx
  type ContextMode (line 21) | type ContextMode = 'off' | 'insights' | 'full'
  type ContextSelections (line 23) | interface ContextSelections {
  function NotebookPage (line 28) | function NotebookPage() {

FILE: frontend/src/app/(dashboard)/notebooks/components/ChatColumn.test.tsx
  function createNotesMock (line 15) | function createNotesMock(overrides: { isLoading?: boolean } = {}) {
  function createChatMock (line 23) | function createChatMock() {

FILE: frontend/src/app/(dashboard)/notebooks/components/ChatColumn.tsx
  type ChatColumnProps (line 14) | interface ChatColumnProps {
  function ChatColumn (line 21) | function ChatColumn({ notebookId, contextSelections, sources, sourcesLoa...

FILE: frontend/src/app/(dashboard)/notebooks/components/NoteEditorDialog.tsx
  type CreateNoteFormData (line 22) | type CreateNoteFormData = z.infer<typeof createNoteSchema>
  type NoteEditorDialogProps (line 24) | interface NoteEditorDialogProps {
  function NoteEditorDialog (line 31) | function NoteEditorDialog({ open, onOpenChange, notebookId, note }: Note...

FILE: frontend/src/app/(dashboard)/notebooks/components/NotebookCard.tsx
  type NotebookCardProps (line 21) | interface NotebookCardProps {
  function NotebookCard (line 25) | function NotebookCard({ notebook }: NotebookCardProps) {

FILE: frontend/src/app/(dashboard)/notebooks/components/NotebookDeleteDialog.tsx
  type NotebookDeleteDialogProps (line 21) | interface NotebookDeleteDialogProps {
  function NotebookDeleteDialog (line 29) | function NotebookDeleteDialog({

FILE: frontend/src/app/(dashboard)/notebooks/components/NotebookHeader.tsx
  type NotebookHeaderProps (line 15) | interface NotebookHeaderProps {
  function NotebookHeader (line 19) | function NotebookHeader({ notebook }: NotebookHeaderProps) {

FILE: frontend/src/app/(dashboard)/notebooks/components/NotebookList.tsx
  type NotebookListProps (line 12) | interface NotebookListProps {
  function NotebookList (line 23) | function NotebookList({

FILE: frontend/src/app/(dashboard)/notebooks/components/NotesColumn.tsx
  type NotesColumnProps (line 28) | interface NotesColumnProps {
  function NotesColumn (line 36) | function NotesColumn({

FILE: frontend/src/app/(dashboard)/notebooks/components/SourcesColumn.tsx
  type SourcesColumnProps (line 27) | interface SourcesColumnProps {
  function SourcesColumn (line 41) | function SourcesColumn({

FILE: frontend/src/app/(dashboard)/notebooks/page.tsx
  function NotebooksPage (line 14) | function NotebooksPage() {

FILE: frontend/src/app/(dashboard)/page.tsx
  function DashboardPage (line 3) | function DashboardPage() {

FILE: frontend/src/app/(dashboard)/podcasts/page.tsx
  function PodcastsPage (line 16) | function PodcastsPage() {

FILE: frontend/src/app/(dashboard)/search/page.tsx
  function SearchPage (line 27) | function SearchPage() {

FILE: frontend/src/app/(dashboard)/settings/api-keys/page.tsx
  type ModelType (line 60) | type ModelType = 'language' | 'embedding' | 'text_to_speech' | 'speech_t...
  constant PROVIDER_DISPLAY_NAMES (line 63) | const PROVIDER_DISPLAY_NAMES: Record<string, string> = {
  constant ALL_PROVIDERS (line 81) | const ALL_PROVIDERS = [
  constant PROVIDER_MODALITIES (line 88) | const PROVIDER_MODALITIES: Record<string, ModelType[]> = {
  constant PROVIDER_DOCS (line 106) | const PROVIDER_DOCS: Record<string, string> = {
  constant TYPE_ICONS (line 122) | const TYPE_ICONS: Record<ModelType, React.ReactNode> = {
  constant TYPE_COLORS (line 129) | const TYPE_COLORS: Record<ModelType, string> = {
  constant TYPE_COLOR_INACTIVE (line 136) | const TYPE_COLOR_INACTIVE = 'bg-muted text-muted-foreground opacity-50'
  constant TYPE_LABELS (line 138) | const TYPE_LABELS: Record<ModelType, string> = {
  function CredentialFormDialog (line 149) | function CredentialFormDialog({
  function DiscoverModelsDialog (line 383) | function DiscoverModelsDialog({
  function DeleteCredentialDialog (line 636) | function DeleteCredentialDialog({
  function CredentialItem (line 739) | function CredentialItem({
  function ProviderSection (line 946) | function ProviderSection({
  function DefaultModelSelectors (line 1046) | function DefaultModelSelectors({
  function ApiKeysPage (line 1271) | function ApiKeysPage() {

FILE: frontend/src/app/(dashboard)/settings/components/SettingsForm.tsx
  type SettingsFormData (line 25) | type SettingsFormData = z.infer<typeof settingsSchema>
  function SettingsForm (line 27) | function SettingsForm() {

FILE: frontend/src/app/(dashboard)/settings/page.tsx
  function SettingsPage (line 10) | function SettingsPage() {

FILE: frontend/src/app/(dashboard)/sources/[id]/page.tsx
  function SourceDetailPage (line 12) | function SourceDetailPage() {

FILE: frontend/src/app/(dashboard)/sources/page.tsx
  function SourcesPage (line 21) | function SourcesPage() {

FILE: frontend/src/app/(dashboard)/transformations/components/DefaultPromptEditor.tsx
  function DefaultPromptEditor (line 13) | function DefaultPromptEditor() {

FILE: frontend/src/app/(dashboard)/transformations/components/TransformationCard.tsx
  type TransformationCardProps (line 15) | interface TransformationCardProps {
  function TransformationCard (line 21) | function TransformationCard({ transformation, onPlayground, onEdit }: Tr...

FILE: frontend/src/app/(dashboard)/transformations/components/TransformationEditorDialog.tsx
  type TransformationFormData (line 28) | type TransformationFormData = z.infer<typeof transformationSchema>
  type TransformationEditorDialogProps (line 30) | interface TransformationEditorDialogProps {
  function TransformationEditorDialog (line 36) | function TransformationEditorDialog({ open, onOpenChange, transformation...

FILE: frontend/src/app/(dashboard)/transformations/components/TransformationPlayground.tsx
  type TransformationPlaygroundProps (line 18) | interface TransformationPlaygroundProps {
  function TransformationPlayground (line 23) | function TransformationPlayground({ transformations, selectedTransformat...

FILE: frontend/src/app/(dashboard)/transformations/components/TransformationsList.tsx
  type TransformationsListProps (line 14) | interface TransformationsListProps {
  function TransformationsList (line 20) | function TransformationsList({ transformations, isLoading, onPlayground ...

FILE: frontend/src/app/(dashboard)/transformations/page.tsx
  function TransformationsPage (line 15) | function TransformationsPage() {

FILE: frontend/src/app/config/route.ts
  function GET (line 25) | async function GET(request: NextRequest) {

FILE: frontend/src/app/layout.tsx
  function RootLayout (line 19) | function RootLayout({

FILE: frontend/src/app/page.tsx
  function HomePage (line 3) | function HomePage() {

FILE: frontend/src/components/auth/LoginForm.tsx
  function LoginForm (line 15) | function LoginForm() {

FILE: frontend/src/components/common/CommandPalette.tsx
  function CommandPalette (line 58) | function CommandPalette() {

FILE: frontend/src/components/common/ConfirmDialog.tsx
  type ConfirmDialogProps (line 16) | interface ConfirmDialogProps {
  function ConfirmDialog (line 27) | function ConfirmDialog({

FILE: frontend/src/components/common/ConnectionGuard.tsx
  type ConnectionGuardProps (line 8) | interface ConnectionGuardProps {
  function ConnectionGuard (line 12) | function ConnectionGuard({ children }: ConnectionGuardProps) {

FILE: frontend/src/components/common/ContextIndicator.tsx
  type ContextIndicatorProps (line 8) | interface ContextIndicatorProps {
  function formatNumber (line 18) | function formatNumber(num: number): string {
  function ContextIndicator (line 28) | function ContextIndicator({

FILE: frontend/src/components/common/ContextToggle.tsx
  type ContextToggleProps (line 15) | interface ContextToggleProps {
  function ContextToggle (line 22) | function ContextToggle({ mode, hasInsights = false, onChange, className ...

FILE: frontend/src/components/common/EmptyState.tsx
  type EmptyStateProps (line 3) | interface EmptyStateProps {
  function EmptyState (line 10) | function EmptyState({ icon: Icon, title, description, action }: EmptySta...

FILE: frontend/src/components/common/ErrorBoundary.tsx
  type ErrorBoundaryState (line 12) | interface ErrorBoundaryState {
  type ErrorBoundaryProps (line 18) | interface ErrorBoundaryProps {
  class ErrorBoundary (line 23) | class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBou...
    method constructor (line 24) | constructor(props: ErrorBoundaryProps) {
    method getDerivedStateFromError (line 29) | static getDerivedStateFromError(error: Error): ErrorBoundaryState {
    method componentDidCatch (line 36) | componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
    method render (line 48) | render() {
  function useErrorBoundary (line 101) | function useErrorBoundary() {

FILE: frontend/src/components/common/InlineEdit.tsx
  type InlineEditProps (line 7) | interface InlineEditProps {
  function InlineEdit (line 20) | function InlineEdit({

FILE: frontend/src/components/common/LanguageLoadingOverlay.tsx
  function LanguageLoadingOverlay (line 20) | function LanguageLoadingOverlay() {

FILE: frontend/src/components/common/LanguageToggle.tsx
  type LanguageToggleProps (line 13) | interface LanguageToggleProps {
  function LanguageToggle (line 17) | function LanguageToggle({ iconOnly = false }: LanguageToggleProps) {

FILE: frontend/src/components/common/LoadingSpinner.tsx
  type LoadingSpinnerProps (line 4) | interface LoadingSpinnerProps {
  function LoadingSpinner (line 9) | function LoadingSpinner({ className, size = 'md' }: LoadingSpinnerProps) {

FILE: frontend/src/components/common/ModelSelector.tsx
  type ModelSelectorProps (line 8) | interface ModelSelectorProps {
  function ModelSelector (line 19) | function ModelSelector({

FILE: frontend/src/components/common/ThemeToggle.tsx
  type ThemeToggleProps (line 14) | interface ThemeToggleProps {
  function ThemeToggle (line 18) | function ThemeToggle({ iconOnly = false }: ThemeToggleProps) {

FILE: frontend/src/components/errors/ConnectionErrorOverlay.tsx
  type ConnectionErrorOverlayProps (line 15) | interface ConnectionErrorOverlayProps {
  function ConnectionErrorOverlay (line 20) | function ConnectionErrorOverlay({

FILE: frontend/src/components/layout/AppShell.tsx
  type AppShellProps (line 6) | interface AppShellProps {
  function AppShell (line 10) | function AppShell({ children }: AppShellProps) {

FILE: frontend/src/components/layout/AppSidebar.tsx
  type CreateTarget (line 77) | type CreateTarget = 'source' | 'notebook' | 'podcast'
  function AppSidebar (line 79) | function AppSidebar() {

FILE: frontend/src/components/layout/SetupBanner.tsx
  function SetupBanner (line 11) | function SetupBanner() {

FILE: frontend/src/components/notebooks/CollapsibleColumn.tsx
  type CollapsibleColumnProps (line 9) | interface CollapsibleColumnProps {
  function CollapsibleColumn (line 17) | function CollapsibleColumn({
  function createCollapseButton (line 69) | function createCollapseButton(onToggle: () => void, label: string) {

FILE: frontend/src/components/notebooks/CreateNotebookDialog.tsx
  type CreateNotebookFormData (line 28) | type CreateNotebookFormData = z.infer<typeof createNotebookSchema>
  type CreateNotebookDialogProps (line 30) | interface CreateNotebookDialogProps {
  function CreateNotebookDialog (line 35) | function CreateNotebookDialog({ open, onOpenChange }: CreateNotebookDial...

FILE: frontend/src/components/podcasts/EpisodeCard.tsx
  type EpisodeCardProps (line 38) | interface EpisodeCardProps {
  function StatusBadge (line 84) | function StatusBadge({ status }: { status?: EpisodeStatus | null }) {
  type OutlineSegment (line 102) | type OutlineSegment = {
  type OutlineData (line 108) | type OutlineData = {
  type TranscriptEntry (line 112) | type TranscriptEntry = {
  type TranscriptData (line 117) | type TranscriptData = {
  function extractOutlineSegments (line 121) | function extractOutlineSegments(outline: unknown): OutlineSegment[] {
  function extractTranscriptEntries (line 131) | function extractTranscriptEntries(transcript: unknown): TranscriptEntry[] {
  function EpisodeCard (line 141) | function EpisodeCard({ episode, onDelete, deleting, onRetry, retrying }:...

FILE: frontend/src/components/podcasts/EpisodeProfilesPanel.tsx
  type EpisodeProfilesPanelProps (line 42) | interface EpisodeProfilesPanelProps {
  function findSpeakerSummary (line 47) | function findSpeakerSummary(
  function EpisodeProfilesPanel (line 54) | function EpisodeProfilesPanel({

FILE: frontend/src/components/podcasts/EpisodesTab.tsx
  function SummaryBadge (line 43) | function SummaryBadge({ label, value }: { label: string; value: number }) {
  function EpisodesTab (line 52) | function EpisodesTab() {

FILE: frontend/src/components/podcasts/GeneratePodcastDialog.tsx
  type SourceMode (line 36) | type SourceMode = 'off' | 'insights' | 'full'
  type NotebookSelection (line 38) | interface NotebookSelection {
  function formatNumber (line 44) | function formatNumber(num: number): string {
  function hasSelections (line 54) | function hasSelections(selection?: NotebookSelection): boolean {
  function getSourceDefaultMode (line 64) | function getSourceDefaultMode(source: SourceListResponse): SourceMode {
  type GeneratePodcastDialogProps (line 68) | interface GeneratePodcastDialogProps {
  type NotebookSummary (line 73) | interface NotebookSummary {
  type ContentSelectionPanelProps (line 79) | interface ContentSelectionPanelProps {
  function ContentSelectionPanel (line 98) | function ContentSelectionPanel({
  function GeneratePodcastDialog (line 397) | function GeneratePodcastDialog({ open, onOpenChange }: GeneratePodcastDi...

FILE: frontend/src/components/podcasts/SpeakerProfilesPanel.tsx
  type SpeakerProfilesPanelProps (line 42) | interface SpeakerProfilesPanelProps {
  function SpeakerProfilesPanel (line 47) | function SpeakerProfilesPanel({

FILE: frontend/src/components/podcasts/TemplatesTab.tsx
  function TemplatesTab (line 12) | function TemplatesTab() {

FILE: frontend/src/components/podcasts/forms/EpisodeProfileFormDialog.tsx
  type EpisodeProfileFormValues (line 52) | type EpisodeProfileFormValues = z.infer<ReturnType<typeof episodeProfile...
  type EpisodeProfileFormDialogProps (line 54) | interface EpisodeProfileFormDialogProps {
  function EpisodeProfileFormDialog (line 62) | function EpisodeProfileFormDialog({

FILE: frontend/src/components/podcasts/forms/SpeakerProfileFormDialog.tsx
  type SpeakerProfileFormValues (line 50) | type SpeakerProfileFormValues = z.infer<ReturnType<typeof speakerProfile...
  type SpeakerProfileFormDialogProps (line 52) | interface SpeakerProfileFormDialogProps {
  constant EMPTY_SPEAKER (line 59) | const EMPTY_SPEAKER = {
  function SpeakerProfileFormDialog (line 67) | function SpeakerProfileFormDialog({

FILE: frontend/src/components/providers/I18nProvider.tsx
  function I18nProvider (line 7) | function I18nProvider({ children }: { children: React.ReactNode }) {

FILE: frontend/src/components/providers/ModalProvider.tsx
  function ModalProvider (line 19) | function ModalProvider() {

FILE: frontend/src/components/providers/QueryProvider.tsx
  type QueryProviderProps (line 6) | interface QueryProviderProps {
  function QueryProvider (line 10) | function QueryProvider({ children }: QueryProviderProps) {

FILE: frontend/src/components/providers/ThemeProvider.tsx
  type ThemeProviderProps (line 6) | interface ThemeProviderProps {
  function ThemeProvider (line 10) | function ThemeProvider({ children }: ThemeProviderProps) {

FILE: frontend/src/components/search/AdvancedModelsDialog.tsx
  type AdvancedModelsDialogProps (line 16) | interface AdvancedModelsDialogProps {
  function AdvancedModelsDialog (line 31) | function AdvancedModelsDialog({

FILE: frontend/src/components/search/SaveToNotebooksDialog.tsx
  type SaveToNotebooksDialogProps (line 20) | interface SaveToNotebooksDialogProps {
  function SaveToNotebooksDialog (line 27) | function SaveToNotebooksDialog({

FILE: frontend/src/components/search/StreamingResponse.tsx
  type StrategyData (line 16) | interface StrategyData {
  type StreamingResponseProps (line 21) | interface StreamingResponseProps {
  function StreamingResponse (line 28) | function StreamingResponse({
  function FinalAnswerContent (line 162) | function FinalAnswerContent({

FILE: frontend/src/components/settings/EmbeddingModelChangeDialog.tsx
  type EmbeddingModelChangeDialogProps (line 19) | interface EmbeddingModelChangeDialogProps {
  function EmbeddingModelChangeDialog (line 27) | function EmbeddingModelChangeDialog({

FILE: frontend/src/components/settings/MigrationBanner.tsx
  type MigrationBannerProps (line 9) | interface MigrationBannerProps {
  function MigrationBanner (line 13) | function MigrationBanner({ providersToMigrate }: MigrationBannerProps) {

FILE: frontend/src/components/settings/ModelTestResultDialog.tsx
  function ModelTestResultDialog (line 15) | function ModelTestResultDialog({

FILE: frontend/src/components/source/ChatPanel.tsx
  type NotebookContextStats (line 27) | interface NotebookContextStats {
  type ChatPanelProps (line 35) | interface ChatPanelProps {
  function ChatPanel (line 59) | function ChatPanel({
  function AIMessageContent (line 328) | function AIMessageContent({

FILE: frontend/src/components/source/MessageActions.tsx
  type MessageActionsProps (line 11) | interface MessageActionsProps {
  function MessageActions (line 16) | function MessageActions({ content, notebookId }: MessageActionsProps) {

FILE: frontend/src/components/source/ModelSelector.tsx
  type ModelSelectorProps (line 27) | interface ModelSelectorProps {
  function ModelSelector (line 33) | function ModelSelector({

FILE: frontend/src/components/source/NotebookAssociations.tsx
  type NotebookAssociationsProps (line 13) | interface NotebookAssociationsProps {
  function NotebookAssociations (line 19) | function NotebookAssociations({

FILE: frontend/src/components/source/SessionManager.tsx
  type SessionManagerProps (line 34) | interface SessionManagerProps {
  function SessionManager (line 44) | function SessionManager({

FILE: frontend/src/components/source/SourceDetailContent.tsx
  type SourceDetailContentProps (line 71) | interface SourceDetailContentProps {
  function SourceDetailContent (line 78) | function SourceDetailContent({

FILE: frontend/src/components/source/SourceDialog.tsx
  type SourceDialogProps (line 7) | interface SourceDialogProps {
  function SourceDialog (line 19) | function SourceDialog({ open, onOpenChange, sourceId }: SourceDialogProp...

FILE: frontend/src/components/source/SourceInsightDialog.tsx
  type SourceInsightDialogProps (line 14) | interface SourceInsightDialogProps {
  function SourceInsightDialog (line 27) | function SourceInsightDialog({ open, onOpenChange, insight, onDelete }: ...

FILE: frontend/src/components/sources/AddExistingSourceDialog.tsx
  type AddExistingSourceDialogProps (line 25) | interface AddExistingSourceDialogProps {
  function AddExistingSourceDialog (line 32) | function AddExistingSourceDialog({

FILE: frontend/src/components/sources/AddSourceButton.tsx
  type AddSourceButtonProps (line 8) | interface AddSourceButtonProps {
  function AddSourceButton (line 16) | function AddSourceButton({

FILE: frontend/src/components/sources/AddSourceDialog.tsx
  constant MAX_BATCH_SIZE (line 28) | const MAX_BATCH_SIZE = 50
  type CreateSourceFormData (line 68) | type CreateSourceFormData = z.infer<typeof createSourceSchema>
  type AddSourceDialogProps (line 70) | interface AddSourceDialogProps {
  type ProcessingState (line 76) | interface ProcessingState {
  type BatchProgress (line 81) | interface BatchProgress {
  function AddSourceDialog (line 88) | function AddSourceDialog({

FILE: frontend/src/components/sources/SourceCard.tsx
  type SourceCardProps (line 35) | interface SourceCardProps {
  constant SOURCE_TYPE_ICONS (line 48) | const SOURCE_TYPE_ICONS = {
  type SourceStatus (line 97) | type SourceStatus = 'new' | 'queued' | 'running' | 'completed' | 'failed'
  function isSourceStatus (line 99) | function isSourceStatus(status: unknown): status is SourceStatus {
  function getSourceType (line 103) | function getSourceType(source: SourceListResponse): 'link' | 'upload' | ...
  function SourceCard (line 110) | function SourceCard({

FILE: frontend/src/components/sources/steps/NotebooksStep.tsx
  type NotebooksStepProps (line 8) | interface NotebooksStepProps {
  function NotebooksStep (line 15) | function NotebooksStep({

FILE: frontend/src/components/sources/steps/ProcessingStep.tsx
  type CreateSourceFormData (line 11) | interface CreateSourceFormData {
  type ProcessingStepProps (line 23) | interface ProcessingStepProps {
  function ProcessingStep (line 32) | function ProcessingStep({

FILE: frontend/src/components/sources/steps/SourceTypeStep.tsx
  type CreateSourceFormData (line 15) | interface CreateSourceFormData {
  function parseUrls (line 28) | function parseUrls(text: string): string[] {
  function validateUrl (line 35) | function validateUrl(url: string): boolean {
  function parseAndValidateUrls (line 44) | function parseAndValidateUrls(text: string): {
  type SourceTypeStepProps (line 89) | interface SourceTypeStepProps {
  constant MAX_BATCH_SIZE (line 98) | const MAX_BATCH_SIZE = 50
  function SourceTypeStep (line 100) | function SourceTypeStep({ control, register, setValue, errors, urlValida...

FILE: frontend/src/components/ui/alert-dialog.tsx
  function AlertDialog (line 9) | function AlertDialog({
  function AlertDialogTrigger (line 15) | function AlertDialogTrigger({
  function AlertDialogPortal (line 23) | function AlertDialogPortal({
  function AlertDialogOverlay (line 31) | function AlertDialogOverlay({
  function AlertDialogContent (line 47) | function AlertDialogContent({
  function AlertDialogHeader (line 66) | function AlertDialogHeader({
  function AlertDialogFooter (line 79) | function AlertDialogFooter({
  function AlertDialogTitle (line 95) | function AlertDialogTitle({
  function AlertDialogDescription (line 108) | function AlertDialogDescription({
  function AlertDialogAction (line 121) | function AlertDialogAction({
  function AlertDialogCancel (line 133) | function AlertDialogCancel({

FILE: frontend/src/components/ui/badge.tsx
  function Badge (line 28) | function Badge({

FILE: frontend/src/components/ui/button.tsx
  function Button (line 38) | function Button({

FILE: frontend/src/components/ui/card.tsx
  function Card (line 5) | function Card({ className, ...props }: React.ComponentProps<"div">) {
  function CardHeader (line 18) | function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
  function CardTitle (line 31) | function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
  function CardDescription (line 41) | function CardDescription({ className, ...props }: React.ComponentProps<"...
  function CardAction (line 51) | function CardAction({ className, ...props }: React.ComponentProps<"div">) {
  function CardContent (line 64) | function CardContent({ className, ...props }: React.ComponentProps<"div"...
  function CardFooter (line 74) | function CardFooter({ className, ...props }: React.ComponentProps<"div">) {

FILE: frontend/src/components/ui/checkbox-list.tsx
  type CheckboxListItem (line 6) | interface CheckboxListItem {
  type CheckboxListProps (line 12) | interface CheckboxListProps {
  function CheckboxList (line 21) | function CheckboxList({

FILE: frontend/src/components/ui/checkbox.tsx
  function Checkbox (line 9) | function Checkbox({

FILE: frontend/src/components/ui/collapsible.tsx
  function Collapsible (line 5) | function Collapsible({
  function CollapsibleTrigger (line 11) | function CollapsibleTrigger({
  function CollapsibleContent (line 22) | function CollapsibleContent({

FILE: frontend/src/components/ui/command.tsx
  function Command (line 16) | function Command({
  function CommandDialog (line 32) | function CommandDialog({
  function CommandInput (line 63) | function CommandInput({
  function CommandList (line 85) | function CommandList({
  function CommandEmpty (line 101) | function CommandEmpty({
  function CommandGroup (line 113) | function CommandGroup({
  function CommandSeparator (line 129) | function CommandSeparator({
  function CommandItem (line 142) | function CommandItem({
  function CommandShortcut (line 158) | function CommandShortcut({

FILE: frontend/src/components/ui/dialog.tsx
  function Dialog (line 10) | function Dialog({
  function DialogTrigger (line 16) | function DialogTrigger({
  function DialogPortal (line 22) | function DialogPortal({
  function DialogClose (line 28) | function DialogClose({
  function DialogOverlay (line 34) | function DialogOverlay({
  function DialogHeader (line 83) | function DialogHeader({ className, ...props }: React.ComponentProps<"div...
  function DialogFooter (line 93) | function DialogFooter({ className, ...props }: React.ComponentProps<"div...
  function DialogTitle (line 106) | function DialogTitle({
  function DialogDescription (line 119) | function DialogDescription({

FILE: frontend/src/components/ui/dropdown-menu.tsx
  function DropdownMenu (line 9) | function DropdownMenu({
  function DropdownMenuPortal (line 15) | function DropdownMenuPortal({
  function DropdownMenuTrigger (line 23) | function DropdownMenuTrigger({
  function DropdownMenuContent (line 34) | function DropdownMenuContent({
  function DropdownMenuGroup (line 54) | function DropdownMenuGroup({
  function DropdownMenuItem (line 62) | function DropdownMenuItem({
  function DropdownMenuCheckboxItem (line 85) | function DropdownMenuCheckboxItem({
  function DropdownMenuRadioGroup (line 111) | function DropdownMenuRadioGroup({
  function DropdownMenuRadioItem (line 122) | function DropdownMenuRadioItem({
  function DropdownMenuLabel (line 146) | function DropdownMenuLabel({
  function DropdownMenuSeparator (line 166) | function DropdownMenuSeparator({
  function DropdownMenuShortcut (line 179) | function DropdownMenuShortcut({
  function DropdownMenuSub (line 195) | function DropdownMenuSub({
  function DropdownMenuSubTrigger (line 201) | function DropdownMenuSubTrigger({
  function DropdownMenuSubContent (line 225) | function DropdownMenuSubContent({

FILE: frontend/src/components/ui/form-section.tsx
  type FormSectionProps (line 7) | interface FormSectionProps {
  function FormSection (line 15) | function FormSection({

FILE: frontend/src/components/ui/input.tsx
  function Input (line 5) | function Input({ className, type, ...props }: React.ComponentProps<"inpu...

FILE: frontend/src/components/ui/label.tsx
  function Label (line 8) | function Label({

FILE: frontend/src/components/ui/markdown-editor.tsx
  type MarkdownEditorProps (line 11) | interface MarkdownEditorProps {

FILE: frontend/src/components/ui/popover.tsx
  function Popover (line 8) | function Popover({
  function PopoverTrigger (line 14) | function PopoverTrigger({
  function PopoverContent (line 20) | function PopoverContent({
  function PopoverAnchor (line 42) | function PopoverAnchor({

FILE: frontend/src/components/ui/progress.tsx
  function Progress (line 8) | function Progress({

FILE: frontend/src/components/ui/radio-group.tsx
  function RadioGroup (line 9) | function RadioGroup({
  function RadioGroupItem (line 22) | function RadioGroupItem({

FILE: frontend/src/components/ui/scroll-area.tsx
  function ScrollArea (line 8) | function ScrollArea({
  function ScrollBar (line 31) | function ScrollBar({

FILE: frontend/src/components/ui/select.tsx
  function Select (line 9) | function Select({
  function SelectGroup (line 15) | function SelectGroup({
  function SelectValue (line 21) | function SelectValue({
  function SelectTrigger (line 27) | function SelectTrigger({
  function SelectContent (line 53) | function SelectContent({
  function SelectLabel (line 88) | function SelectLabel({
  function SelectItem (line 101) | function SelectItem({
  function SelectSeparator (line 125) | function SelectSeparator({
  function SelectScrollUpButton (line 138) | function SelectScrollUpButton({
  function SelectScrollDownButton (line 156) | function SelectScrollDownButton({

FILE: frontend/src/components/ui/separator.tsx
  function Separator (line 8) | function Separator({

FILE: frontend/src/components/ui/tabs.tsx
  function Tabs (line 8) | function Tabs({
  function TabsList (line 21) | function TabsList({
  function TabsTrigger (line 37) | function TabsTrigger({
  function TabsContent (line 53) | function TabsContent({

FILE: frontend/src/components/ui/textarea.tsx
  function Textarea (line 5) | function Textarea({ className, ...props }: React.ComponentProps<"textare...

FILE: frontend/src/components/ui/tooltip.tsx
  function TooltipProvider (line 8) | function TooltipProvider({
  function Tooltip (line 21) | function Tooltip({
  function TooltipTrigger (line 31) | function TooltipTrigger({
  function TooltipContent (line 37) | function TooltipContent({

FILE: frontend/src/components/ui/wizard-container.tsx
  type WizardStep (line 6) | interface WizardStep {
  type WizardContainerProps (line 12) | interface WizardContainerProps {
  function StepIndicator (line 20) | function StepIndicator({ currentStep, steps, onStepClick }: {
  function WizardContainer (line 80) | function WizardContainer({

FILE: frontend/src/lib/api/credentials.ts
  type Credential (line 4) | interface Credential {
  type CreateCredentialRequest (line 25) | interface CreateCredentialRequest {
  type UpdateCredentialRequest (line 42) | interface UpdateCredentialRequest {
  type DiscoveredModel (line 58) | interface DiscoveredModel {
  type RegisterModelData (line 65) | interface RegisterModelData {
  type DiscoverModelsResponse (line 71) | interface DiscoverModelsResponse {
  type RegisterModelsRequest (line 77) | interface RegisterModelsRequest {
  type RegisterModelsResponse (line 81) | interface RegisterModelsResponse {
  type TestConnectionResult (line 86) | interface TestConnectionResult {
  type CredentialDeleteResponse (line 92) | interface CredentialDeleteResponse {
  type MigrationResult (line 97) | interface MigrationResult {
  type CredentialStatus (line 105) | interface CredentialStatus {
  type EnvStatus (line 111) | type EnvStatus = Record<string, boolean>

FILE: frontend/src/lib/api/embedding.ts
  type EmbedContentRequest (line 3) | interface EmbedContentRequest {
  type EmbedContentResponse (line 9) | interface EmbedContentResponse {
  type RebuildEmbeddingsRequest (line 16) | interface RebuildEmbeddingsRequest {
  type RebuildEmbeddingsResponse (line 23) | interface RebuildEmbeddingsResponse {
  type RebuildProgress (line 29) | interface RebuildProgress {
  type RebuildStats (line 38) | interface RebuildStats {
  type RebuildStatusResponse (line 50) | interface RebuildStatusResponse {

FILE: frontend/src/lib/api/insights.ts
  type SourceInsightResponse (line 3) | interface SourceInsightResponse {
  type CreateSourceInsightRequest (line 12) | interface CreateSourceInsightRequest {
  type InsightCreationResponse (line 16) | interface InsightCreationResponse {
  type CommandJobStatusResponse (line 24) | interface CommandJobStatusResponse {

FILE: frontend/src/lib/api/podcasts.ts
  type EpisodeProfileInput (line 12) | type EpisodeProfileInput = Omit<EpisodeProfile, 'id'>
  type SpeakerProfileInput (line 13) | type SpeakerProfileInput = Omit<SpeakerProfile, 'id'>
  function resolvePodcastAssetUrl (line 15) | async function resolvePodcastAssetUrl(path?: string | null): Promise<str...

FILE: frontend/src/lib/api/query-client.ts
  constant QUERY_KEYS (line 17) | const QUERY_KEYS = {

FILE: frontend/src/lib/config.ts
  constant BUILD_TIME (line 9) | const BUILD_TIME = new Date().toISOString()
  function getApiUrl (line 22) | async function getApiUrl(): Promise<string> {
  function getConfig (line 43) | async function getConfig(): Promise<AppConfig> {
  function fetchConfig (line 59) | async function fetchConfig(): Promise<AppConfig> {
  function resetConfig (line 145) | function resetConfig(): void {

FILE: frontend/src/lib/hooks/use-ask.ts
  type AskModels (line 10) | interface AskModels {
  type StrategyData (line 16) | interface StrategyData {
  type AskState (line 21) | interface AskState {
  function useAsk (line 29) | function useAsk() {

FILE: frontend/src/lib/hooks/use-auth.ts
  function useAuth (line 7) | function useAuth() {

FILE: frontend/src/lib/hooks/use-create-dialogs.tsx
  type CreateDialogsContextType (line 8) | interface CreateDialogsContextType {
  function CreateDialogsProvider (line 16) | function CreateDialogsProvider({ children }: { children: ReactNode }) {
  function useCreateDialogs (line 41) | function useCreateDialogs() {

FILE: frontend/src/lib/hooks/use-credentials.ts
  constant CREDENTIAL_QUERY_KEYS (line 15) | const CREDENTIAL_QUERY_KEYS = {
  function useCredentialStatus (line 26) | function useCredentialStatus() {
  function useEnvStatus (line 36) | function useEnvStatus() {
  function useCredentials (line 46) | function useCredentials(provider?: string) {
  function useCredentialsByProvider (line 57) | function useCredentialsByProvider(provider: string) {
  function useCredential (line 68) | function useCredential(credentialId: string) {
  function useCreateCredential (line 79) | function useCreateCredential() {
  function useUpdateCredential (line 107) | function useUpdateCredential() {
  function useDeleteCredential (line 141) | function useDeleteCredential() {
  function useTestCredential (line 176) | function useTestCredential() {
  function useDiscoverModels (line 224) | function useDiscoverModels() {
  function useRegisterModels (line 243) | function useRegisterModels() {
  function useMigrateFromEnv (line 287) | function useMigrateFromEnv() {
  function useMigrateFromProviderConfig (line 340) | function useMigrateFromProviderConfig() {

FILE: frontend/src/lib/hooks/use-insights.ts
  function useInsight (line 4) | function useInsight(id: string, options?: { enabled?: boolean }) {

FILE: frontend/src/lib/hooks/use-media-query.ts
  function useMediaQuery (line 9) | function useMediaQuery(query: string): boolean {
  function useIsDesktop (line 30) | function useIsDesktop(): boolean {

FILE: frontend/src/lib/hooks/use-modal-manager.ts
  type ModalType (line 5) | type ModalType = 'source' | 'note' | 'insight'
  function useModalManager (line 7) | function useModalManager() {

FILE: frontend/src/lib/hooks/use-models.ts
  constant MODEL_QUERY_KEYS (line 9) | const MODEL_QUERY_KEYS = {
  function useModels (line 16) | function useModels() {
  function useModel (line 23) | function useModel(id: string) {
  function useCreateModel (line 31) | function useCreateModel() {
  function useDeleteModel (line 55) | function useDeleteModel() {
  function useModelDefaults (line 81) | function useModelDefaults() {
  function useUpdateModelDefaults (line 88) | function useUpdateModelDefaults() {
  function useProviders (line 112) | function useProviders() {
  function useAutoAssignDefaults (line 119) | function useAutoAssignDefaults() {
  function useTestModel (line 160) | function useTestModel() {

FILE: frontend/src/lib/hooks/use-navigation.ts
  function useNavigation (line 3) | function useNavigation() {

FILE: frontend/src/lib/hooks/use-notebooks.ts
  function useNotebooks (line 9) | function useNotebooks(archived?: boolean) {
  function useNotebook (line 16) | function useNotebook(id: string) {
  function useCreateNotebook (line 24) | function useCreateNotebook() {
  function useUpdateNotebook (line 48) | function useUpdateNotebook() {
  function useNotebookDeletePreview (line 74) | function useNotebookDeletePreview(id: string, enabled: boolean = false) {
  function useDeleteNotebook (line 82) | function useDeleteNotebook() {

FILE: frontend/src/lib/hooks/use-notes.ts
  function useNotes (line 9) | function useNotes(notebookId?: string) {
  function useNote (line 17) | function useNote(id?: string, options?: { enabled?: boolean }) {
  function useCreateNote (line 26) | function useCreateNote() {
  function useUpdateNote (line 52) | function useUpdateNote() {
  function useDeleteNote (line 78) | function useDeleteNote() {

FILE: frontend/src/lib/hooks/use-podcasts.ts
  function useLanguages (line 19) | function useLanguages() {
  type EpisodeStatusCounts (line 27) | interface EpisodeStatusCounts {
  function hasActiveEpisodes (line 35) | function hasActiveEpisodes(episodes: PodcastEpisode[]) {
  function usePodcastEpisodes (line 42) | function usePodcastEpisodes(options?: { autoRefresh?: boolean }) {
  function useRetryPodcastEpisode (line 91) | function useRetryPodcastEpisode() {
  function useDeletePodcastEpisode (line 115) | function useDeletePodcastEpisode() {
  function useEpisodeProfiles (line 139) | function useEpisodeProfiles() {
  function useCreateEpisodeProfile (line 151) | function useCreateEpisodeProfile() {
  function useUpdateEpisodeProfile (line 177) | function useUpdateEpisodeProfile() {
  function useDeleteEpisodeProfile (line 208) | function useDeleteEpisodeProfile() {
  function useDuplicateEpisodeProfile (line 233) | function useDuplicateEpisodeProfile() {
  function useSpeakerProfiles (line 259) | function useSpeakerProfiles(episodeProfiles?: EpisodeProfile[]) {
  function useCreateSpeakerProfile (line 279) | function useCreateSpeakerProfile() {
  function useUpdateSpeakerProfile (line 306) | function useUpdateSpeakerProfile() {
  function useDeleteSpeakerProfile (line 338) | function useDeleteSpeakerProfile() {
  function useDuplicateSpeakerProfile (line 364) | function useDuplicateSpeakerProfile() {
  function useGeneratePodcast (line 389) | function useGeneratePodcast() {

FILE: frontend/src/lib/hooks/use-search.ts
  function useSearch (line 8) | function useSearch() {

FILE: frontend/src/lib/hooks/use-settings.ts
  function useSettings (line 9) | function useSettings() {
  function useUpdateSettings (line 16) | function useUpdateSettings() {

FILE: frontend/src/lib/hooks/use-sources.ts
  constant NOTEBOOK_SOURCES_PAGE_SIZE (line 16) | const NOTEBOOK_SOURCES_PAGE_SIZE = 30
  function useSources (line 18) | function useSources(notebookId?: string) {
  function useNotebookSources (line 32) | function useNotebookSources(notebookId: string) {
  function useSource (line 79) | function useSource(id: string) {
  function useCreateSource (line 89) | function useCreateSource() {
  function useUpdateSource (line 141) | function useUpdateSource() {
  function useDeleteSource (line 168) | function useDeleteSource() {
  function useFileUpload (line 195) | function useFileUpload() {
  function useSourceStatus (line 222) | function useSourceStatus(sourceId: string, enabled = true) {
  function useRetrySource (line 249) | function useRetrySource() {
  function useAddSourcesToNotebook (line 280) | function useAddSourcesToNotebook() {
  function useRemoveSourceFromNotebook (line 342) | function useRemoveSourceFromNotebook() {

FILE: frontend/src/lib/hooks/use-toast.ts
  type ToastProps (line 4) | type ToastProps = {
  function useToast (line 10) | function useToast() {

FILE: frontend/src/lib/hooks/use-transformations.ts
  constant TRANSFORMATION_QUERY_KEYS (line 13) | const TRANSFORMATION_QUERY_KEYS = {
  function useTransformations (line 19) | function useTransformations() {
  function useTransformation (line 26) | function useTransformation(id?: string, options?: { enabled?: boolean }) {
  function useCreateTransformation (line 35) | function useCreateTransformation() {
  function useUpdateTransformation (line 59) | function useUpdateTransformation() {
  function useDeleteTransformation (line 85) | function useDeleteTransformation() {
  function useExecuteTransformation (line 109) | function useExecuteTransformation() {
  function useDefaultPrompt (line 125) | function useDefaultPrompt() {
  function useUpdateDefaultPrompt (line 132) | function useUpdateDefaultPrompt() {

FILE: frontend/src/lib/hooks/use-translation.ts
  function useTranslation (line 15) | function useTranslation() {

FILE: frontend/src/lib/hooks/use-version-check.ts
  function useVersionCheck (line 14) | function useVersionCheck() {

FILE: frontend/src/lib/hooks/useNotebookChat.ts
  type UseNotebookChatParams (line 19) | interface UseNotebookChatParams {
  function useNotebookChat (line 26) | function useNotebookChat({ notebookId, sources, notes, contextSelections...

FILE: frontend/src/lib/hooks/useSourceChat.ts
  function useSourceChat (line 17) | function useSourceChat(sourceId: string) {

FILE: frontend/src/lib/i18n-events.ts
  constant I18N_LANGUAGE_CHANGE_START (line 1) | const I18N_LANGUAGE_CHANGE_START = 'i18n:language-change-start'
  constant I18N_LANGUAGE_CHANGE_END (line 2) | const I18N_LANGUAGE_CHANGE_END = 'i18n:language-change-end'
  type LanguageChangeDetail (line 4) | type LanguageChangeDetail = {
  function emitLanguageChangeStart (line 10) | function emitLanguageChangeStart(language: string) {
  function emitLanguageChangeEnd (line 18) | function emitLanguageChangeEnd(language: string) {

FILE: frontend/src/lib/locales/index.ts
  type TranslationKeys (line 23) | type TranslationKeys = typeof enUS;
  type LanguageCode (line 25) | type LanguageCode = 'zh-CN' | 'en-US' | 'zh-TW' | 'pt-BR' | 'ja-JP' | 'i...
  type Language (line 27) | type Language = {

FILE: frontend/src/lib/stores/auth-store.ts
  type AuthState (line 5) | interface AuthState {

FILE: frontend/src/lib/stores/navigation-store.ts
  type NavigationState (line 4) | interface NavigationState {

FILE: frontend/src/lib/stores/notebook-columns-store.ts
  type NotebookColumnsState (line 4) | interface NotebookColumnsState {

FILE: frontend/src/lib/stores/sidebar-store.ts
  type SidebarState (line 4) | interface SidebarState {

FILE: frontend/src/lib/stores/theme-store.ts
  type Theme (line 4) | type Theme = 'light' | 'dark' | 'system'
  type ThemeState (line 6) | interface ThemeState {
  function useTheme (line 52) | function useTheme() {

FILE: frontend/src/lib/types/api.ts
  type NotebookResponse (line 1) | interface NotebookResponse {
  type NoteResponse (line 12) | interface NoteResponse {
  type SourceListResponse (line 21) | interface SourceListResponse {
  type SourceDetailResponse (line 41) | interface SourceDetailResponse extends SourceListResponse {
  type SourceResponse (line 46) | type SourceResponse = SourceDetailResponse
  type SourceStatusResponse (line 48) | interface SourceStatusResponse {
  type SettingsResponse (line 55) | interface SettingsResponse {
  type CreateNotebookRequest (line 63) | interface CreateNotebookRequest {
  type UpdateNotebookRequest (line 68) | interface UpdateNotebookRequest {
  type NotebookDeletePreview (line 74) | interface NotebookDeletePreview {
  type NotebookDeleteResponse (line 82) | interface NotebookDeleteResponse {
  type CreateNoteRequest (line 89) | interface CreateNoteRequest {
  type CreateSourceRequest (line 96) | interface CreateSourceRequest {
  type UpdateNoteRequest (line 114) | interface UpdateNoteRequest {
  type UpdateSourceRequest (line 120) | interface UpdateSourceRequest {
  type APIError (line 127) | interface APIError {
  type BaseChatSession (line 133) | interface BaseChatSession {
  type SourceChatSession (line 142) | interface SourceChatSession extends BaseChatSession {
  type SourceChatMessage (line 147) | interface SourceChatMessage {
  type SourceChatContextIndicator (line 154) | interface SourceChatContextIndicator {
  type SourceChatSessionWithMessages (line 160) | interface SourceChatSessionWithMessages extends SourceChatSession {
  type CreateSourceChatSessionRequest (line 165) | interface CreateSourceChatSessionRequest {
  type UpdateSourceChatSessionRequest (line 171) | interface UpdateSourceChatSessionRequest {
  type SendMessageRequest (line 176) | interface SendMessageRequest {
  type SourceChatStreamEvent (line 181) | interface SourceChatStreamEvent {
  type NotebookChatSession (line 190) | interface NotebookChatSession extends BaseChatSession {
  type NotebookChatMessage (line 194) | interface NotebookChatMessage {
  type NotebookChatSessionWithMessages (line 201) | interface NotebookChatSessionWithMessages extends NotebookChatSession {
  type CreateNotebookChatSessionRequest (line 205) | interface CreateNotebookChatSessionRequest {
  type UpdateNotebookChatSessionRequest (line 211) | interface UpdateNotebookChatSessionRequest {
  type SendNotebookChatMessageRequest (line 216) | interface SendNotebookChatMessageRequest {
  type BuildContextRequest (line 226) | interface BuildContextRequest {
  type BuildContextResponse (line 234) | interface BuildContextResponse {

FILE: frontend/src/lib/types/auth.ts
  type AuthState (line 1) | interface AuthState {
  type LoginCredentials (line 8) | interface LoginCredentials {

FILE: frontend/src/lib/types/common.ts
  type NavItem (line 3) | interface NavItem {
  type PageProps (line 9) | interface PageProps {

FILE: frontend/src/lib/types/config.ts
  type BackendConfigResponse (line 6) | interface BackendConfigResponse {
  type AppConfig (line 17) | interface AppConfig {
  type ConnectionError (line 29) | interface ConnectionError {

FILE: frontend/src/lib/types/models.ts
  type Model (line 1) | interface Model {
  type CreateModelRequest (line 11) | interface CreateModelRequest {
  type ModelDefaults (line 18) | interface ModelDefaults {
  type ProviderAvailability (line 28) | interface ProviderAvailability {
  type DiscoveredModel (line 35) | interface DiscoveredModel {
  type ProviderSyncResult (line 42) | interface ProviderSyncResult {
  type AllProvidersSyncResult (line 49) | interface AllProvidersSyncResult {
  type ProviderModelCount (line 55) | interface ProviderModelCount {
  type AutoAssignResult (line 61) | interface AutoAssignResult {
  type ModelTestResult (line 67) | interface ModelTestResult {

FILE: frontend/src/lib/types/podcasts.ts
  type EpisodeStatus (line 1) | type EpisodeStatus =
  type EpisodeProfile (line 11) | interface EpisodeProfile {
  type SpeakerVoiceConfig (line 28) | interface SpeakerVoiceConfig {
  type SpeakerProfile (line 36) | interface SpeakerProfile {
  type Language (line 47) | interface Language {
  type PodcastEpisode (line 52) | interface PodcastEpisode {
  type PodcastGenerationRequest (line 67) | interface PodcastGenerationRequest {
  type PodcastGenerationResponse (line 76) | interface PodcastGenerationResponse {
  type EpisodeStatusGroup (line 84) | type EpisodeStatusGroup = 'running' | 'completed' | 'failed' | 'pending'
  type EpisodeStatusGroups (line 86) | type EpisodeStatusGroups = Record<EpisodeStatusGroup, PodcastEpisode[]>
  constant ACTIVE_EPISODE_STATUSES (line 88) | const ACTIVE_EPISODE_STATUSES: EpisodeStatus[] = [
  constant FAILED_EPISODE_STATUSES (line 95) | const FAILED_EPISODE_STATUSES: EpisodeStatus[] = ['failed', 'error']
  function groupEpisodesByStatus (line 97) | function groupEpisodesByStatus(episodes: PodcastEpisode[]): EpisodeStatu...
  function speakerUsageMap (line 124) | function speakerUsageMap(
  function needsModelSetup (line 149) | function needsModelSetup(profile: EpisodeProfile | SpeakerProfile): bool...

FILE: frontend/src/lib/types/search.ts
  type SearchRequest (line 2) | interface SearchRequest {
  type SearchResult (line 11) | interface SearchResult {
  type SearchResponse (line 26) | interface SearchResponse {
  type AskRequest (line 33) | interface AskRequest {
  type AskResponse (line 40) | interface AskResponse {
  type StrategyData (line 46) | interface StrategyData {
  type AskStreamEvent (line 54) | interface AskStreamEvent {

FILE: frontend/src/lib/types/transformations.ts
  type Transformation (line 1) | interface Transformation {
  type CreateTransformationRequest (line 12) | interface CreateTransformationRequest {
  type UpdateTransformationRequest (line 20) | interface UpdateTransformationRequest {
  type ExecuteTransformationRequest (line 28) | interface ExecuteTransformationRequest {
  type ExecuteTransformationResponse (line 34) | interface ExecuteTransformationResponse {
  type DefaultPrompt (line 40) | interface DefaultPrompt {

FILE: frontend/src/lib/utils.ts
  function cn (line 4) | function cn(...inputs: ClassValue[]) {

FILE: frontend/src/lib/utils/date-locale.ts
  constant LOCALE_MAP (line 7) | const LOCALE_MAP: Record<string, Locale> = {
  function getDateLocale (line 25) | function getDateLocale(language: string): Locale {

FILE: frontend/src/lib/utils/error-handler.ts
  constant ERROR_MAP (line 4) | const ERROR_MAP: Record<string, string> = {
  function getApiErrorKey (line 34) | function getApiErrorKey(errorOrMessage: unknown, fallbackKey?: string): ...
  function getApiErrorMessage (line 59) | function getApiErrorMessage(
  function formatApiError (line 82) | function formatApiError(error: unknown): string {

FILE: frontend/src/lib/utils/source-references.tsx
  type ReferenceType (line 4) | type ReferenceType = 'source' | 'note' | 'source_insight'
  type ParsedReference (line 6) | interface ParsedReference {
  type ExtractedReference (line 16) | interface ExtractedReference {
  type ExtractedReferences (line 23) | interface ExtractedReferences {
  type ReferenceData (line 28) | interface ReferenceData {
  function parseSourceReferences (line 46) | function parseSourceReferences(text: string): ParsedReference[] {
  function convertSourceReferences (line 76) | function convertSourceReferences(
  function convertReferencesToMarkdownLinks (line 174) | function convertReferencesToMarkdownLinks(text: string): string {
  function createReferenceLinkComponent (line 266) | function createReferenceLinkComponent(
  function convertReferencesToCompactMarkdown (line 339) | function convertReferencesToCompactMarkdown(text: string, referencesLabe...
  function createCompactReferenceLinkComponent (line 431) | function createCompactReferenceLinkComponent(
  function convertSourceReferencesLegacy (line 482) | function convertSourceReferencesLegacy(text: string): React.ReactNode {

FILE: frontend/src/proxy.ts
  function proxy (line 4) | function proxy(request: NextRequest) {

FILE: open_notebook/ai/connection_tester.py
  function _test_azure_connection (line 40) | async def _test_azure_connection(
  function _test_ollama_connection (line 97) | async def _test_ollama_connection(base_url: str) -> Tuple[bool, str]:
  function _test_openai_compatible_connection (line 132) | async def _test_openai_compatible_connection(base_url: str, api_key: Opt...
  function test_provider_connection (line 170) | async def test_provider_connection(
  function _generate_test_wav (line 313) | def _generate_test_wav() -> io.BytesIO:
  function _normalize_error_message (line 347) | def _normalize_error_message(error_msg: str) -> Tuple[bool, str]:
  function test_individual_model (line 367) | async def test_individual_model(model) -> Tuple[bool, str]:

FILE: open_notebook/ai/key_provider.py
  function _get_default_credential (line 68) | async def _get_default_credential(provider: str) -> Optional[Credential]:
  function get_api_key (line 79) | async def get_api_key(provider: str) -> Optional[str]:
  function _provision_simple_provider (line 105) | async def _provision_simple_provider(provider: str) -> bool:
  function _provision_vertex (line 137) | async def _provision_vertex() -> bool:
  function _provision_azure (line 166) | async def _provision_azure() -> bool:
  function _provision_openai_compatible (line 211) | async def _provision_openai_compatible() -> bool:
  function provision_provider_keys (line 236) | async def provision_provider_keys(provider: str) -> bool:
  function provision_all_keys (line 273) | async def provision_all_keys() -> dict[str, bool]:

FILE: open_notebook/ai/model_discovery.py
  class DiscoveredModel (line 22) | class DiscoveredModel:
  function classify_model_type (line 135) | def classify_model_type(model_name: str, provider: str) -> str:
  function discover_openai_models (line 173) | async def discover_openai_models() -> List[DiscoveredModel]:
  function discover_anthropic_models (line 207) | async def discover_anthropic_models() -> List[DiscoveredModel]:
  function discover_google_models (line 226) | async def discover_google_models() -> List[DiscoveredModel]:
  function discover_ollama_models (line 269) | async def discover_ollama_models() -> List[DiscoveredModel]:
  function discover_groq_models (line 302) | async def discover_groq_models() -> List[DiscoveredModel]:
  function discover_mistral_models (line 336) | async def discover_mistral_models() -> List[DiscoveredModel]:
  function discover_deepseek_models (line 375) | async def discover_deepseek_models() -> List[DiscoveredModel]:
  function discover_xai_models (line 409) | async def discover_xai_models() -> List[DiscoveredModel]:
  function discover_openrouter_models (line 443) | async def discover_openrouter_models() -> List[DiscoveredModel]:
  function discover_voyage_models (line 478) | async def discover_voyage_models() -> List[DiscoveredModel]:
  function discover_elevenlabs_models (line 500) | async def discover_elevenlabs_models() -> List[DiscoveredModel]:
  function discover_openai_compatible_models (line 521) | async def discover_openai_compatible_models() -> List[DiscoveredModel]:
  function discover_provider_models (line 608) | async def discover_provider_models(provider: str) -> List[DiscoveredModel]:
  function sync_provider_models (line 632) | async def sync_provider_models(
  function sync_all_providers (line 699) | async def sync_all_providers() -> Dict[str, Tuple[int, int, int]]:
  function get_provider_model_count (line 727) | async def get_provider_model_count(provider: str) -> Dict[str, int]:

FILE: open_notebook/ai/models.py
  class Model (line 19) | class Model(ObjectModel):
    method get_models_by_type (line 28) | async def get_models_by_type(cls, model_type):
    method get_by_credential (line 35) | async def get_by_credential(cls, credential_id: str):
    method _prepare_save_data (line 43) | def _prepare_save_data(self) -> Dict[str, Any]:
    method get_credential_obj (line 49) | async def get_credential_obj(self):
  class DefaultModels (line 62) | class DefaultModels(RecordModel):
    method get_instance (line 74) | async def get_instance(cls) -> "DefaultModels":
  class ModelManager (line 98) | class ModelManager:
    method __init__ (line 99) | def __init__(self):
    method get_model (line 102) | async def get_model(self, model_id: str, **kwargs) -> Optional[ModelTy...
    method get_defaults (line 178) | async def get_defaults(self) -> DefaultModels:
    method get_speech_to_text (line 185) | async def get_speech_to_text(self, **kwargs) -> Optional[SpeechToTextM...
    method get_text_to_speech (line 197) | async def get_text_to_speech(self, **kwargs) -> Optional[TextToSpeechM...
    method get_embedding_model (line 209) | async def get_embedding_model(self, **kwargs) -> Optional[EmbeddingMod...
    method get_default_model (line 221) | async def get_default_model(self, model_type: str, **kwargs) -> Option...

FILE: open_notebook/ai/provision.py
  function provision_langchain_model (line 10) | async def provision_langchain_model(

FILE: open_notebook/database/async_migrate.py
  class AsyncMigration (line 13) | class AsyncMigration:
    method __init__ (line 18) | def __init__(self, sql: str) -> None:
    method from_file (line 23) | def from_file(cls, file_path: str) -> "AsyncMigration":
    method run (line 36) | async def run(self, bump: bool = True) -> None:
  class AsyncMigrationRunner (line 52) | class AsyncMigrationRunner:
    method __init__ (line 57) | def __init__(
    method run_all (line 66) | async def run_all(self) -> None:
    method run_one_up (line 74) | async def run_one_up(self) -> None:
    method run_one_down (line 82) | async def run_one_down(self) -> None:
  class AsyncMigrationManager (line 91) | class AsyncMigrationManager:
    method __init__ (line 96) | def __init__(self):
    method get_current_version (line 171) | async def get_current_version(self) -> int:
    method needs_migration (line 175) | async def needs_migration(self) -> bool:
    method run_migration_up (line 180) | async def run_migration_up(self):
  function get_latest_version (line 198) | async def get_latest_version() -> int:
  function get_all_versions (line 210) | async def get_all_versions() -> List[dict]:
  function bump_version (line 220) | async def bump_version() -> None:
  function lower_version (line 230) | async def lower_version() -> None:

FILE: open_notebook/database/migrate.py
  class MigrationManager (line 6) | class MigrationManager:
    method __init__ (line 11) | def __init__(self):
    method get_current_version (line 15) | def get_current_version(self) -> int:
    method needs_migration (line 20) | def needs_migration(self) -> bool:
    method run_migration_up (line 24) | def run_migration_up(self):

FILE: open_notebook/database/repository.py
  function get_database_url (line 12) | def get_database_url():
  function get_database_password (line 24) | def get_database_password():
  function parse_record_ids (line 29) | def parse_record_ids(obj: Any) -> Any:
  function ensure_record_id (line 40) | def ensure_record_id(value: Union[str, RecordID]) -> RecordID:
  function db_connection (line 48) | async def db_connection():
  function repo_query (line 65) | async def repo_query(
  function repo_create (line 85) | async def repo_create(table: str, data: Dict[str, Any]) -> Dict[str, Any]:
  function repo_relate (line 106) | async def repo_relate(
  function repo_upsert (line 123) | async def repo_upsert(
  function repo_update (line 134) | async def repo_update(
  function repo_delete (line 158) | async def repo_delete(record_id: Union[str, RecordID]):
  function repo_insert (line 169) | async def repo_insert(

FILE: open_notebook/domain/base.py
  class ObjectModel (line 31) | class ObjectModel(BaseModel):
    method get_all (line 39) | async def get_all(cls: Type[T], order_by=None) -> List[T]:
    method get (line 70) | async def get(cls: Type[T], id: str) -> T:
    method _get_class_by_table_name (line 98) | def _get_class_by_table_name(cls, table_name: str) -> Optional[Type["O...
    method save (line 113) | async def save(self) -> None:
    method _prepare_save_data (line 162) | def _prepare_save_data(self) -> Dict[str, Any]:
    method delete (line 170) | async def delete(self) -> bool:
    method relate (line 184) | async def relate(
    method parse_datetime (line 200) | def parse_datetime(cls, value):
  class RecordModel (line 206) | class RecordModel(BaseModel):
    method __new__ (line 221) | def __new__(cls, **kwargs):
    method __init__ (line 236) | def __init__(self, **kwargs):
    method _load_from_db (line 249) | async def _load_from_db(self):
    method get_instance (line 275) | async def get_instance(cls) -> "RecordModel":
    method auto_save_validator (line 282) | def auto_save_validator(self):
    method update (line 290) | async def update(self):
    method clear_instance (line 319) | def clear_instance(cls):
    method patch (line 324) | async def patch(self, model_dict: dict):

FILE: open_notebook/domain/content_settings.py
  class ContentSettings (line 8) | class ContentSettings(RecordModel):

FILE: open_notebook/domain/credential.py
  class Credential (line 29) | class Credential(ObjectModel):
    method to_esperanto_config (line 67) | def to_esperanto_config(self) -> Dict[str, Any]:
    method get_by_provider (line 100) | async def get_by_provider(cls, provider: str) -> List["Credential"]:
    method get (line 116) | async def get(cls, id: str) -> "Credential":
    method get_all (line 132) | async def get_all(cls, order_by=None) -> List["Credential"]:
    method get_linked_models (line 146) | async def get_linked_models(self) -> list:
    method _prepare_save_data (line 158) | def _prepare_save_data(self) -> Dict[str, Any]:
    method save (line 174) | async def save(self) -> None:
    method _from_db_row (line 191) | def _from_db_row(cls, row: dict) -> "Credential":

FILE: open_notebook/domain/notebook.py
  class Notebook (line 16) | class Notebook(ObjectModel):
    method name_must_not_be_empty (line 24) | def name_must_not_be_empty(cls, v):
    method get_sources (line 29) | async def get_sources(self) -> List["Source"]:
    method get_notes (line 46) | async def get_notes(self) -> List["Note"]:
    method get_chat_sessions (line 63) | async def get_chat_sessions(self) -> List["ChatSession"]:
    method get_delete_preview (line 88) | async def get_delete_preview(self) -> Dict[str, Any]:
    method delete (line 138) | async def delete(self, delete_exclusive_sources: bool = False) -> Dict...
  class Asset (line 233) | class Asset(BaseModel):
  class SourceEmbedding (line 238) | class SourceEmbedding(ObjectModel):
    method get_source (line 242) | async def get_source(self) -> "Source":
  class SourceInsight (line 257) | class SourceInsight(ObjectModel):
    method get_source (line 262) | async def get_source(self) -> "Source":
    method save_as_note (line 276) | async def save_as_note(self, notebook_id: Optional[str] = None) -> Any:
  class Source (line 288) | class Source(ObjectModel):
    method parse_command (line 302) | def parse_command(cls, value):
    method parse_id (line 310) | def parse_id(cls, value):
    method get_status (line 318) | async def get_status(self) -> Optional[str]:
    method get_processing_progress (line 332) | async def get_processing_progress(self) -> Optional[Dict[str, Any]]:
    method get_context (line 361) | async def get_context(
    method get_embedded_chunks (line 376) | async def get_embedded_chunks(self) -> int:
    method get_insights (line 392) | async def get_insights(self) -> List[SourceInsight]:
    method add_to_notebook (line 406) | async def add_to_notebook(self, notebook_id: str) -> Any:
    method vectorize (line 411) | async def vectorize(self) -> str:
    method add_insight (line 459) | async def add_insight(self, insight_type: str, content: str) -> Option...
    method _prepare_save_data (line 506) | def _prepare_save_data(self) -> dict:
    method delete (line 516) | async def delete(self) -> bool:
  class Note (line 557) | class Note(ObjectModel):
    method content_must_not_be_empty (line 565) | def content_must_not_be_empty(cls, v):
    method save (line 570) | async def save(self) -> Optional[str]:
    method add_to_notebook (line 595) | async def add_to_notebook(self, notebook_id: str) -> Any:
    method get_context (line 600) | def get_context(
  class ChatSession (line 613) | class ChatSession(ObjectModel):
    method relate_to_notebook (line 619) | async def relate_to_notebook(self, notebook_id: str) -> Any:
    method relate_to_source (line 624) | async def relate_to_source(self, source_id: str) -> Any:
  function text_search (line 630) | async def text_search(
  function vector_search (line 650) | async def vector_search(

FILE: open_notebook/domain/provider_config.py
  class ProviderCredential (line 22) | class ProviderCredential:
    method __init__ (line 51) | def __init__(
    method to_dict (line 91) | def to_dict(self, encrypted: bool = False) -> dict:
    method from_dict (line 130) | def from_dict(cls, data: dict, decrypted: bool = False) -> "ProviderCr...
  class ProviderConfig (line 175) | class ProviderConfig(RecordModel):
    method get_instance (line 198) | async def get_instance(cls) -> "ProviderConfig":
    method get_default_config (line 280) | def get_default_config(self, provider: str) -> Optional[ProviderCreden...
    method get_config (line 304) | def get_config(
    method add_config (line 326) | def add_config(self, provider: str, credential: ProviderCredential) ->...
    method delete_config (line 357) | def delete_config(self, provider: str, config_id: str) -> bool:
    method set_default_config (line 384) | def set_default_config(self, provider: str, config_id: str) -> bool:
    method _prepare_save_data (line 411) | def _prepare_save_data(self) -> dict:
    method save (line 429) | async def save(self) -> "ProviderConfig":
    method _clear_for_test (line 441) | def _clear_for_test(cls) -> None:

FILE: open_notebook/domain/transformation.py
  class Transformation (line 8) | class Transformation(ObjectModel):
  class DefaultPrompts (line 17) | class DefaultPrompts(RecordModel):

FILE: open_notebook/exceptions.py
  class OpenNotebookError (line 1) | class OpenNotebookError(Exception):
  class DatabaseOperationError (line 7) | class DatabaseOperationError(OpenNotebookError):
  class UnsupportedTypeException (line 13) | class UnsupportedTypeException(OpenNotebookError):
  class InvalidInputError (line 19) | class InvalidInputError(OpenNotebookError):
  class NotFoundError (line 25) | class NotFoundError(OpenNotebookError):
  class AuthenticationError (line 31) | class AuthenticationError(OpenNotebookError):
  class ConfigurationError (line 37) | class ConfigurationError(OpenNotebookError):
  class ExternalServiceError (line 43) | class ExternalServiceError(OpenNotebookError):
  class RateLimitError (line 49) | class RateLimitError(OpenNotebookError):
  class FileOperationError (line 55) | class FileOperationError(OpenNotebookError):
  class NetworkError (line 61) | class NetworkError(OpenNotebookError):
  class NoTranscriptFound (line 67) | class NoTranscriptFound(OpenNotebookError):

FILE: open_notebook/graphs/ask.py
  class SubGraphState (line 20) | class SubGraphState(TypedDict):
  class Search (line 29) | class Search(BaseModel):
  class Strategy (line 36) | class Strategy(BaseModel):
  class ThreadState (line 44) | class ThreadState(TypedDict):
  function call_model_with_messages (line 51) | async def call_model_with_messages(state: ThreadState, config: RunnableC...
  function trigger_queries (line 83) | async def trigger_queries(state: ThreadState, config: RunnableConfig):
  function provide_answer (line 98) | async def provide_answer(state: SubGraphState, config: RunnableConfig) -...
  function write_final_answer (line 127) | async def write_final_answer(state: ThreadState, config: RunnableConfig)...

FILE: open_notebook/graphs/chat.py
  class ThreadState (line 22) | class ThreadState(TypedDict):
  function call_model_with_messages (line 30) | def call_model_with_messages(state: ThreadState, config: RunnableConfig)...

FILE: open_notebook/graphs/prompt.py
  class PatternChainState (line 13) | class PatternChainState(TypedDict):
  function call_model (line 20) | async def call_model(state: dict, config: RunnableConfig) -> dict:

FILE: open_notebook/graphs/source.py
  class SourceState (line 19) | class SourceState(TypedDict):
  class TransformationState (line 29) | class TransformationState(TypedDict):
  function content_process (line 34) | async def content_process(state: SourceState) -> dict:
  function save_source (line 97) | async def save_source(state: SourceState) -> dict:
  function trigger_transformations (line 130) | def trigger_transformations(state: SourceState, config: RunnableConfig) ...
  function transform_content (line 149) | async def transform_content(state: TransformationState) -> Optional[dict]:

FILE: open_notebook/graphs/source_chat.py
  class SourceChatState (line 23) | class SourceChatState(TypedDict):
  function call_model_with_source_context (line 33) | def call_model_with_source_context(
  function _call_model_with_source_context_inner (line 54) | def _call_model_with_source_context_inner(
  function _format_source_context (line 190) | def _format_source_context(context_data: Dict) -> str:

FILE: open_notebook/graphs/tools.py
  function get_current_timestamp (line 8) | def get_current_timestamp() -> str:

FILE: open_notebook/graphs/transformation.py
  class TransformationState (line 16) | class TransformationState(TypedDict):
  function run_transformation (line 23) | async def run_transformation(state: dict, config: RunnableConfig) -> dict:

FILE: open_notebook/podcasts/migration.py
  function _find_model_record (line 14) | async def _find_model_record(
  function _find_or_create_model (line 27) | async def _find_or_create_model(
  function migrate_podcast_profiles (line 65) | async def migrate_podcast_profiles() -> None:

FILE: open_notebook/podcasts/models.py
  function _resolve_model_config (line 11) | async def _resolve_model_config(model_id: str) -> Tuple[str, str, dict]:
  class EpisodeProfile (line 32) | class EpisodeProfile(ObjectModel):
    method validate_segments (line 84) | def validate_segments(cls, v):
    method _prepare_save_data (line 89) | def _prepare_save_data(self) -> dict:
    method resolve_outline_config (line 97) | async def resolve_outline_config(self) -> Tuple[str, str, dict]:
    method resolve_transcript_config (line 106) | async def resolve_transcript_config(self) -> Tuple[str, str, dict]:
    method get_by_name (line 116) | async def get_by_name(cls, name: str) -> Optional["EpisodeProfile"]:
  class SpeakerProfile (line 126) | class SpeakerProfile(ObjectModel):
    method validate_speakers (line 160) | def validate_speakers(cls, v):
    method _prepare_save_data (line 171) | def _prepare_save_data(self) -> dict:
    method resolve_tts_config (line 182) | async def resolve_tts_config(self) -> Tuple[str, str, dict]:
    method get_by_name (line 192) | async def get_by_name(cls, name: str) -> Optional["SpeakerProfile"]:
  class PodcastEpisode (line 202) | class PodcastEpisode(ObjectModel):
    method get_job_status (line 231) | async def get_job_status(self) -> Optional[str]:
    method get_job_detail (line 244) | async def get_job_detail(self) -> dict:
    method parse_command (line 264) | def parse_command(cls, value):
    method _prepare_save_data (line 269) | def _prepare_save_data(self) -> dict:

FILE: open_notebook/utils/chunking.py
  function _get_chunk_size (line 30) | def _get_chunk_size() -> int:
  function _get_chunk_overlap (line 57) | def _get_chunk_overlap(chunk_size: int) -> int:
  class ContentType (line 95) | class ContentType(Enum):
  function detect_content_type_from_extension (line 141) | def detect_content_type_from_extension(
  function detect_content_type_from_heuristics (line 163) | def detect_content_type_from_heuristics(text: str) -> Tuple[ContentType,...
  function _calculate_html_score (line 198) | def _calculate_html_score(text: str) -> float:
  function _calculate_markdown_score (line 234) | def _calculate_markdown_score(text: str) -> float:
  function detect_content_type (line 290) | def detect_content_type(text: str, file_path: Optional[str] = None) -> C...
  function _get_html_splitter (line 333) | def _get_html_splitter() -> HTMLHeaderTextSplitter:
  function _get_markdown_splitter (line 343) | def _get_markdown_splitter() -> MarkdownHeaderTextSplitter:
  function _get_plain_splitter (line 356) | def _get_plain_splitter() -> RecursiveCharacterTextSplitter:
  function _apply_secondary_chunking (line 366) | def _apply_secondary_chunking(chunks: List[str]) -> List[str]:
  function chunk_text (line 386) | def chunk_text(

FILE: open_notebook/utils/context_builder.py
  class ContextItem (line 22) | class ContextItem:
    method __post_init__ (line 31) | def __post_init__(self):
  class ContextConfig (line 39) | class ContextConfig:
    method __post_init__ (line 49) | def __post_init__(self):
  class ContextBuilder (line 59) | class ContextBuilder:
    method __init__ (line 65) | def __init__(self, **kwargs):
    method build (line 105) | async def build(self) -> Dict[str, Any]:
    method _add_source_context (line 142) | async def _add_source_context(
    method _add_notebook_context (line 210) | async def _add_notebook_context(self, notebook_id: str) -> None:
    method _add_note_context (line 254) | async def _add_note_context(
    method _process_custom_params (line 296) | async def _process_custom_params(self) -> None:
    method add_item (line 305) | def add_item(self, item: ContextItem) -> None:
    method prioritize (line 315) | def prioritize(self) -> None:
    method truncate_to_fit (line 320) | def truncate_to_fit(self, max_tokens: int) -> None:
    method remove_duplicates (line 351) | def remove_duplicates(self) -> None:
    method _format_response (line 367) | def _format_response(self) -> Dict[str, Any]:
  function build_notebook_context (line 422) | async def build_notebook_context(
  function build_source_context (line 444) | async def build_source_context(
  function build_mixed_context (line 464) | async def build_mixed_context(

FILE: open_notebook/utils/embedding.py
  function mean_pool_embeddings (line 31) | async def mean_pool_embeddings(embeddings: List[List[float]]) -> List[fl...
  function generate_embeddings (line 87) | async def generate_embeddings(
  function generate_embedding (line 168) | async def generate_embedding(

FILE: open_notebook/utils/encryption.py
  function get_secret_from_env (line 29) | def get_secret_from_env(var_name: str) -> Optional[str]:
  function _get_or_create_encryption_key (line 62) | def _get_or_create_encryption_key() -> str:
  function _get_encryption_key (line 96) | def _get_encryption_key() -> str:
  function _ensure_fernet_key (line 104) | def _ensure_fernet_key(key: str) -> str:
  function get_fernet (line 115) | def get_fernet() -> Fernet:
  function encrypt_value (line 128) | def encrypt_value(value: str) -> str:
  function looks_like_fernet_token (line 145) | def looks_like_fernet_token(s: str) -> bool:
  function decrypt_value (line 167) | def decrypt_value(value: str) -> str:

FILE: open_notebook/utils/error_classifier.py
  function classify_error (line 72) | def classify_error(exception: BaseException) -> tuple[type[OpenNotebookE...
  function _truncate (line 99) | def _truncate(text: str, max_length: int = 200) -> str:

FILE: open_notebook/utils/graph_utils.py
  function get_session_message_count (line 7) | async def get_session_message_count(graph, session_id: str) -> int:

FILE: open_notebook/utils/text_utils.py
  function remove_non_ascii (line 17) | def remove_non_ascii(text: str) -> str:
  function remove_non_printable (line 22) | def remove_non_printable(text: str) -> str:
  function parse_thinking_content (line 42) | def parse_thinking_content(content: str) -> Tuple[str, str]:
  function clean_thinking_content (line 100) | def clean_thinking_content(content: str) -> str:
  function extract_text_content (line 122) | def extract_text_content(content) -> str:

FILE: open_notebook/utils/token_utils.py
  function token_count (line 15) | def token_count(input_string: str) -> int:
  function token_cost (line 43) | def token_cost(token_count: int, cost_per_million: float = 0.150) -> float:

FILE: open_notebook/utils/version_utils.py
  function get_version_from_github_async (line 14) | async def get_version_from_github_async(repo_url: str, branch: str = "ma...
  function get_version_from_github (line 59) | def get_version_from_github(repo_url: str, branch: str = "main") -> str:
  function get_installed_version (line 113) | def get_installed_version(package_name: str) -> str:
  function compare_versions (line 132) | def compare_versions(version1: str, version2: str) -> int:

FILE: scripts/export_docs.py
  function get_markdown_files (line 20) | def get_markdown_files(folder: Path) -> List[Path]:
  function consolidate_folder (line 26) | def consolidate_folder(folder: Path, output_dir: Path) -> None:
  function main (line 60) | def main():

FILE: tests/test_chunking.py
  class TestDetectContentTypeFromExtension (line 23) | class TestDetectContentTypeFromExtension:
    method test_html_extensions (line 26) | def test_html_extensions(self):
    method test_markdown_extensions (line 33) | def test_markdown_extensions(self):
    method test_plain_text_extensions (line 40) | def test_plain_text_extensions(self):
    method test_code_extensions_as_plain (line 45) | def test_code_extensions_as_plain(self):
    method test_unknown_extensions (line 52) | def test_unknown_extensions(self):
    method test_no_extension (line 58) | def test_no_extension(self):
    method test_none_input (line 63) | def test_none_input(self):
    method test_empty_string (line 67) | def test_empty_string(self):
  class TestDetectContentTypeFromHeuristics (line 77) | class TestDetectContentTypeFromHeuristics:
    method test_html_detection_doctype (line 80) | def test_html_detection_doctype(self):
    method test_html_detection_tags (line 87) | def test_html_detection_tags(self):
    method test_markdown_detection_headers (line 94) | def test_markdown_detection_headers(self):
    method test_markdown_detection_links (line 114) | def test_markdown_detection_links(self):
    method test_markdown_detection_code_blocks (line 127) | def test_markdown_detection_code_blocks(self):
    method test_plain_text_detection (line 142) | def test_plain_text_detection(self):
    method test_short_text (line 151) | def test_short_text(self):
    method test_empty_text (line 156) | def test_empty_text(self):
  class TestDetectContentType (line 167) | class TestDetectContentType:
    method test_extension_takes_priority (line 170) | def test_extension_takes_priority(self):
    method test_no_extension_uses_heuristics (line 179) | def test_no_extension_uses_heuristics(self):
    method test_extension_html (line 185) | def test_extension_html(self):
    method test_extension_markdown (line 190) | def test_extension_markdown(self):
    method test_high_confidence_override (line 195) | def test_high_confidence_override(self):
  class TestChunkText (line 209) | class TestChunkText:
    method test_empty_text (line 212) | def test_empty_text(self):
    method test_short_text_no_chunking (line 217) | def test_short_text_no_chunking(self):
    method test_text_at_chunk_limit (line 224) | def test_text_at_chunk_limit(self):
    method test_long_text_is_chunked (line 230) | def test_long_text_is_chunked(self):
    method test_explicit_content_type_html (line 240) | def test_explicit_content_type_html(self):
    method test_explicit_content_type_markdown (line 253) | def test_explicit_content_type_markdown(self):
    method test_explicit_content_type_plain (line 270) | def test_explicit_content_type_plain(self):
    method test_file_path_detection (line 276) | def test_file_path_detection(self):
    method test_secondary_chunking_for_large_sections (line 282) | def test_secondary_chunking_for_large_sections(self):

FILE: tests/test_domain.py
  class TestRecordModelSingleton (line 28) | class TestRecordModelSingleton:
    method test_recordmodel_singleton_behavior (line 31) | def test_recordmodel_singleton_behavior(self):
  class TestModelManager (line 59) | class TestModelManager:
    method test_model_manager_instance_isolation (line 62) | def test_model_manager_instance_isolation(self):
  class TestNotebookDomain (line 77) | class TestNotebookDomain:
    method test_notebook_name_validation (line 80) | def test_notebook_name_validation(self):
    method test_notebook_archived_flag (line 94) | def test_notebook_archived_flag(self):
  class TestSourceDomain (line 108) | class TestSourceDomain:
    method test_source_command_field_parsing (line 111) | def test_source_command_field_parsing(self):
    method test_source_delete_cleans_up_file (line 127) | async def test_source_delete_cleans_up_file(self):
    method test_source_delete_without_file (line 167) | async def test_source_delete_without_file(self):
    method test_source_delete_continues_on_file_error (line 184) | async def test_source_delete_continues_on_file_error(self):
    method test_vectorize_raises_valueerror_when_no_text (line 206) | async def test_vectorize_raises_valueerror_when_no_text(self):
    method test_vectorize_raises_valueerror_when_empty_string (line 213) | async def test_vectorize_raises_valueerror_when_empty_string(self):
    method test_vectorize_raises_valueerror_when_whitespace_only (line 220) | async def test_vectorize_raises_valueerror_when_whitespace_only(self):
    method test_vectorize_submits_command_with_valid_text (line 227) | async def test_vectorize_submits_command_with_valid_text(self):
  class TestNoteDomain (line 247) | class TestNoteDomain:
    method test_note_content_validation (line 250) | def test_note_content_validation(self):
    method test_note_content_for_embedding (line 268) | def test_note_content_for_embedding(self):
  class TestPodcastDomain (line 287) | class TestPodcastDomain:
    method test_speaker_profile_validation (line 290) | def test_speaker_profile_validation(self):
  class TestTransformationDomain (line 344) | class TestTransformationDomain:
    method test_transformation_creation (line 347) | def test_transformation_creation(self):
  class TestContentSettings (line 366) | class TestContentSettings:
    method test_content_settings_defaults (line 369) | def test_content_settings_defaults(self):
  class TestEpisodeProfile (line 385) | class TestEpisodeProfile:
    method test_episode_profile_segment_validation (line 388) | def test_episode_profile_segment_validation(self):

FILE: tests/test_embedding.py
  class TestMeanPoolEmbeddings (line 20) | class TestMeanPoolEmbeddings:
    method test_single_embedding (line 24) | async def test_single_embedding(self):
    method test_two_embeddings (line 35) | async def test_two_embeddings(self):
    method test_identical_embeddings (line 49) | async def test_identical_embeddings(self):
    method test_empty_list_raises (line 64) | async def test_empty_list_raises(self):
    method test_normalization (line 70) | async def test_normalization(self):
    method test_high_dimensional (line 83) | async def test_high_dimensional(self):
  class TestGenerateEmbeddings (line 105) | class TestGenerateEmbeddings:
    method test_empty_list (line 109) | async def test_empty_list(self):
    method test_no_model_raises (line 115) | async def test_no_model_raises(self):
    method test_successful_embedding (line 128) | async def test_successful_embedding(self):
  class TestGenerateEmbedding (line 152) | class TestGenerateEmbedding:
    method test_empty_text_raises (line 156) | async def test_empty_text_raises(self):
    method test_short_text_direct_embedding (line 165) | async def test_short_text_direct_embedding(self):
    method test_long_text_chunked_and_pooled (line 183) | async def test_long_text_chunked_and_pooled(self):
    method test_content_type_parameter (line 211) | async def test_content_type_parameter(self):
    method test_batching (line 233) | async def test_batching(self):
    method test_batch_retry_on_transient_failure (line 265) | async def test_batch_retry_on_transient_failure(self):
    method test_batch_retry_exhaustion (line 294) | async def test_batch_retry_exhaustion(self):
  class TestErrorClassifier413 (line 323) | class TestErrorClassifier413:
    method test_413_status_code (line 326) | def test_413_status_code(self):
    method test_request_entity_too_large (line 335) | def test_request_entity_too_large(self):

FILE: tests/test_graphs.py
  class TestGraphTools (line 27) | class TestGraphTools:
    method test_get_current_timestamp_format (line 30) | def test_get_current_timestamp_format(self):
    method test_get_current_timestamp_validity (line 38) | def test_get_current_timestamp_validity(self):
    method test_get_current_timestamp_is_tool (line 62) | def test_get_current_timestamp_is_tool(self):
  class TestPromptGraph (line 74) | class TestPromptGraph:
    method test_pattern_chain_state_structure (line 77) | def test_pattern_chain_state_structure(self):
    method test_prompt_graph_compilation (line 88) | def test_prompt_graph_compilation(self):
  class TestTransformationGraph (line 102) | class TestTransformationGraph:
    method test_transformation_state_structure (line 105) | def test_transformation_state_structure(self):
    method test_run_transformation_assertion_no_content (line 128) | async def test_run_transformation_assertion_no_content(self):
    method test_transformation_graph_compilation (line 147) | def test_transformation_graph_compilation(self):

FILE: tests/test_models_api.py
  function client (line 8) | def client():
  class TestModelCreation (line 15) | class TestModelCreation:
    method test_create_duplicate_model_same_case (line 21) | async def test_create_duplicate_model_same_case(
    method test_create_duplicate_model_different_case (line 50) | async def test_create_duplicate_model_different_case(
    method test_create_same_model_name_different_provider (line 78) | async def test_create_same_model_name_different_provider(
    method test_create_same_model_name_different_type (line 100) | async def test_create_same_model_name_different_type(self, mock_repo_q...
  class TestModelsProviderAvailability (line 119) | class TestModelsProviderAvailability:
    method test_generic_env_var_enables_all_modes (line 124) | def test_generic_env_var_enables_all_modes(self, mock_esperanto, mock_...
    method test_mode_specific_env_vars_llm_embedding (line 162) | def test_mode_specific_env_vars_llm_embedding(
    method test_no_env_vars_set (line 204) | def test_no_env_vars_set(self, mock_esperanto, mock_env, client):
    method test_mixed_config_generic_and_mode_specific (line 233) | def test_mixed_config_generic_and_mode_specific(
    method test_individual_mode_llm_only (line 275) | def test_individual_mode_llm_only(self, mock_esperanto, mock_env, clie...
    method test_individual_mode_embedding_only (line 305) | def test_individual_mode_embedding_only(self, mock_esperanto, mock_env...
    method test_individual_mode_stt_only (line 335) | def test_individual_mode_stt_only(self, mock_esperanto, mock_env, clie...
    method test_individual_mode_tts_only (line 365) | def test_individual_mode_tts_only(self, mock_esperanto, mock_env, clie...

FILE: tests/test_notes_api.py
  function client (line 8) | def client():
  class TestNoteCreation (line 15) | class TestNoteCreation:
    method test_create_note_returns_command_id (line 19) | def test_create_note_returns_command_id(self, mock_note_cls, client):
    method test_create_note_command_id_none_when_no_content_embedding (line 43) | def test_create_note_command_id_none_when_no_content_embedding(
  class TestNoteUpdate (line 68) | class TestNoteUpdate:
    method test_update_note_returns_command_id (line 72) | def test_update_note_returns_command_id(self, mock_note_cls, client):
    method test_update_note_command_id_none_when_no_embedding (line 94) | def test_update_note_command_id_none_when_no_embedding(

FILE: tests/test_podcast_path.py
  class TestBuildEpisodeOutputDir (line 15) | class TestBuildEpisodeOutputDir:
    method test_directory_name_is_valid_uuid (line 18) | def test_directory_name_is_valid_uuid(self):
    method test_path_structure (line 23) | def test_path_structure(self):
    method test_no_collision_between_calls (line 27) | def test_no_collision_between_calls(self):
    method test_path_is_independent_of_episode_name (line 32) | def test_path_is_independent_of_episode_name(self):
    method test_path_works_on_posix (line 58) | def test_path_works_on_posix(self):
    method test_directory_can_be_created (line 63) | def test_directory_can_be_created(self, tmp_path):

FILE: tests/test_url_validation.py
  class TestUrlValidation (line 19) | class TestUrlValidation:
    method test_valid_https_url (line 22) | def test_valid_https_url(self):
    method test_valid_http_url (line 28) | def test_valid_http_url(self):
    method test_invalid_scheme (line 33) | def test_invalid_scheme(self):
    method test_localhost_allowed_for_self_hosted (line 41) | def test_localhost_allowed_for_self_hosted(self):
    method test_localhost_allowed_for_ollama (line 48) | def test_localhost_allowed_for_ollama(self):
    method test_private_ip_allowed_for_self_hosted (line 54) | def test_private_ip_allowed_for_self_hosted(self):
    method test_private_ip_allowed_for_ollama (line 62) | def test_private_ip_allowed_for_ollama(self):
    method test_loopback_allowed_for_self_hosted (line 68) | def test_loopback_allowed_for_self_hosted(self):
    method test_link_local_rejection (line 73) | def test_link_local_rejection(self):
    method test_ipv6_localhost_allowed (line 82) | def test_ipv6_localhost_allowed(self):
    method test_empty_url (line 87) | def test_empty_url(self):
    method test_invalid_url_format (line 93) | def test_invalid_url_format(self):
    method test_public_hostnames_allowed (line 98) | def test_public_hostnames_allowed(self):
    method test_azure_specific_urls (line 106) | def test_azure_specific_urls(self):
    method test_openai_compatible_urls (line 115) | def test_openai_compatible_urls(self):
    method test_ipv4_mapped_ipv6_link_local_rejected (line 122) | def test_ipv4_mapped_ipv6_link_local_rejected(self):
    method test_ipv4_mapped_ipv6_private_allowed (line 127) | def test_ipv4_mapped_ipv6_private_allowed(self):

FILE: tests/test_utils.py
  class TestTextUtilities (line 26) | class TestTextUtilities:
    method test_remove_non_ascii (line 29) | def test_remove_non_ascii(self):
    method test_remove_non_ascii_pure_ascii (line 40) | def test_remove_non_ascii_pure_ascii(self):
    method test_remove_non_printable (line 46) | def test_remove_non_printable(self):
    method test_remove_non_printable_preserves_newlines (line 57) | def test_remove_non_printable_preserves_newlines(self):
    method test_parse_thinking_content_basic (line 64) | def test_parse_thinking_content_basic(self):
    method test_parse_thinking_content_multiple_tags (line 72) | def test_parse_thinking_content_multiple_tags(self):
    method test_parse_thinking_content_no_tags (line 83) | def test_parse_thinking_content_no_tags(self):
    method test_parse_thinking_content_malformed_no_open_tag (line 91) | def test_parse_thinking_content_malformed_no_open_tag(self):
    method test_parse_thinking_content_invalid_input (line 99) | def test_parse_thinking_content_invalid_input(self):
    method test_parse_thinking_content_large_content (line 111) | def test_parse_thinking_content_large_content(self):
    method test_clean_thinking_content (line 120) | def test_clean_thinking_content(self):
  class TestTokenUtilities (line 135) | class TestTokenUtilities:
    method test_token_count_fallback (line 138) | def test_token_count_fallback(self):
    method test_token_count_network_error_fallback (line 154) | def test_token_count_network_error_fallback(self):
  class TestVersionUtilities (line 182) | class TestVersionUtilities:
    method test_compare_versions_equal (line 185) | def test_compare_versions_equal(self):
    method test_compare_versions_less_than (line 190) | def test_compare_versions_less_than(self):
    method test_compare_versions_greater_than (line 201) | def test_compare_versions_greater_than(self):
    method test_compare_versions_prerelease (line 212) | def test_compare_versions_prerelease(self):
    method test_get_installed_version_success (line 220) | def test_get_installed_version_success(self):
    method test_get_installed_version_not_found (line 229) | def test_get_installed_version_not_found(self):
    method test_get_version_from_github_invalid_url (line 236) | def test_get_version_from_github_invalid_url(self):
  class TestContextBuilder (line 252) | class TestContextBuilder:
    method test_context_config_defaults (line 255) | def test_context_config_defaults(self):
    method test_context_builder_initialization (line 268) | def test_context_builder_initialization(self):
Condensed preview — 446 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (2,999K chars).
[
  {
    "path": ".dockerignore",
    "chars": 496,
    "preview": "# Git\n.git\n.gitignore\n\n# Python\n__pycache__\n*.pyc\n*.pyo\n*.pyd\n.venv\nvenv\nENV\nenv\n.pytest_cache\n.mypy_cache\n.ruff_cache\n\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "chars": 3408,
    "preview": "name: 🐛 Bug Report\ndescription: Report a bug or unexpected behavior (app is running but misbehaving)\ntitle: \"[Bug]: \"\nla"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "chars": 524,
    "preview": "blank_issues_enabled: false\ncontact_links:\n  - name: 💬 Discord Community\n    url: https://discord.gg/37XJPXfz2w\n    abou"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.yml",
    "chars": 2154,
    "preview": "name: ✨ Feature Suggestion\ndescription: Suggest a new feature or improvement for Open Notebook\ntitle: \"[Feature]: \"\nlabe"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/installation_issue.yml",
    "chars": 5467,
    "preview": "name: 🔧 Installation Issue\ndescription: Report problems with installation, setup, or connectivity\ntitle: \"[Install]: \"\nl"
  },
  {
    "path": ".github/pull_request_template.md",
    "chars": 3507,
    "preview": "## Description\n\n<!-- Provide a clear and concise description of what this PR does -->\n\n## Related Issue\n\n<!-- This PR sh"
  },
  {
    "path": ".github/workflows/build-and-release.yml",
    "chars": 12020,
    "preview": "name: Build and Release\n\non:\n  workflow_dispatch:\n    inputs:\n      push_latest:\n        description: 'Also push v1-late"
  },
  {
    "path": ".github/workflows/build-dev.yml",
    "chars": 10339,
    "preview": "name: Development Build\n\non:\n  pull_request:\n    branches: [ main ]\n  push:\n    branches: [ main ]\n    paths-ignore:\n   "
  },
  {
    "path": ".github/workflows/claude-code-review.yml",
    "chars": 1742,
    "preview": "name: Claude Code Review\n\non:\n  pull_request:\n    types: [opened, synchronize, ready_for_review, reopened]\n  pull_reques"
  },
  {
    "path": ".github/workflows/claude.yml",
    "chars": 1940,
    "preview": "name: Claude Code\n\non:\n  issue_comment:\n    types: [created]\n  pull_request_review_comment:\n    types: [created]\n  issue"
  },
  {
    "path": ".github/workflows/test.yml",
    "chars": 1111,
    "preview": "name: Tests\n\non:\n  pull_request:\n    branches: [main]\n  push:\n    branches: [main]\n    paths-ignore:\n      - '**.md'\n   "
  },
  {
    "path": ".gitignore",
    "chars": 1368,
    "preview": ".env\nprompts/patterns/user/\n/notebooks/\ndata/\n.uploads/\nsqlite-db/\nsurreal-data/\ndocker.env\nnotebook_data/\n# Python-spec"
  },
  {
    "path": ".python-version",
    "chars": 5,
    "preview": "3.12\n"
  },
  {
    "path": ".worktreeinclude",
    "chars": 70,
    "preview": ".env\n.env.local\n.env.*\n**/.claude/settings.local.json\nCLAUDE.local.md\n"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 13172,
    "preview": "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Change"
  },
  {
    "path": "CLAUDE.md",
    "chars": 9170,
    "preview": "# Open Notebook - Root CLAUDE.md\n\nThis file provides architectural guidance for contributors working on Open Notebook at"
  },
  {
    "path": "CONFIGURATION.md",
    "chars": 1398,
    "preview": "# Configuration Guide\n\n**📍 This file has moved!**\n\nAll configuration documentation has been consolidated into the new do"
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 1067,
    "preview": "# Contributing to Open Notebook\n\n**📍 This file has moved!**\n\nAll contribution guidelines have been consolidated into the"
  },
  {
    "path": "Dockerfile",
    "chars": 4507,
    "preview": "# Build stage\nFROM python:3.12-slim-bookworm AS builder\n\n# Install uv using the official method\nCOPY --from=ghcr.io/astr"
  },
  {
    "path": "Dockerfile.single",
    "chars": 3368,
    "preview": "# Stage 1: Frontend Builder\nFROM node:20-slim AS frontend-builder\nWORKDIR /app/frontend\n\n# Copy dependency files first t"
  },
  {
    "path": "LICENSE",
    "chars": 1061,
    "preview": "MIT License\nCopyright (c) 2024 Luis Novo\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof"
  },
  {
    "path": "MAINTAINER_GUIDE.md",
    "chars": 683,
    "preview": "# Maintainer Guide\n\n**📍 This file has moved!**\n\nAll maintainer guidelines have been consolidated into the new developmen"
  },
  {
    "path": "Makefile",
    "chars": 7001,
    "preview": ".PHONY: run frontend check ruff database lint api start-all stop-all status clean-cache worker worker-start worker-stop "
  },
  {
    "path": "README.dev.md",
    "chars": 9486,
    "preview": "# Developer Guide\n\nThis guide is for developers working on Open Notebook. For end-user documentation, see [README.md](RE"
  },
  {
    "path": "README.md",
    "chars": 17683,
    "preview": "<a id=\"readme-top\"></a>\n\n<!-- [![Contributors][contributors-shield]][contributors-url] -->\n[![Forks][forks-shield]][fork"
  },
  {
    "path": "api/CLAUDE.md",
    "chars": 13809,
    "preview": "# API Module\n\nFastAPI-based REST backend exposing services for notebooks, sources, notes, chat, podcasts, and AI model m"
  },
  {
    "path": "api/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "api/auth.py",
    "chars": 3822,
    "preview": "from typing import Optional\n\nfrom fastapi import Depends, HTTPException, Request\nfrom fastapi.security import HTTPAuthor"
  },
  {
    "path": "api/chat_service.py",
    "chars": 5995,
    "preview": "\"\"\"\nChat service for API operations.\nProvides async interface for chat functionality.\n\"\"\"\n\nimport os\nfrom typing import "
  },
  {
    "path": "api/client.py",
    "chars": 20131,
    "preview": "\"\"\"\nAPI client for Open Notebook API.\nThis module provides a client interface to interact with the Open Notebook API.\n\"\""
  },
  {
    "path": "api/command_service.py",
    "chars": 3680,
    "preview": "from typing import Any, Dict, List, Optional\n\nfrom loguru import logger\nfrom surreal_commands import get_command_status,"
  },
  {
    "path": "api/context_service.py",
    "chars": 728,
    "preview": "\"\"\"\nContext service layer using API.\n\"\"\"\n\nfrom typing import Any, Dict, List, Optional, Union\n\nfrom loguru import logger"
  },
  {
    "path": "api/credentials_service.py",
    "chars": 33532,
    "preview": "\"\"\"\nCredentials Service\n\nBusiness logic for managing AI provider credentials.\nExtracted from the credentials router to f"
  },
  {
    "path": "api/embedding_service.py",
    "chars": 654,
    "preview": "\"\"\"\nEmbedding service layer using API.\n\"\"\"\n\nfrom typing import Any, Dict, List, Union\n\nfrom loguru import logger\n\nfrom a"
  },
  {
    "path": "api/episode_profiles_service.py",
    "chars": 4282,
    "preview": "\"\"\"\nEpisode profiles service layer using API.\n\"\"\"\n\nfrom typing import List\n\nfrom loguru import logger\n\nfrom api.client i"
  },
  {
    "path": "api/insights_service.py",
    "chars": 3549,
    "preview": "\"\"\"\nInsights service layer using API.\n\"\"\"\n\nfrom typing import List, Optional\n\nfrom loguru import logger\n\nfrom api.client"
  },
  {
    "path": "api/main.py",
    "chars": 9507,
    "preview": "# Load environment variables\nfrom dotenv import load_dotenv\n\nload_dotenv()\n\nfrom contextlib import asynccontextmanager\n\n"
  },
  {
    "path": "api/models.py",
    "chars": 24048,
    "preview": "from typing import Any, Dict, List, Literal, Optional\n\nfrom pydantic import BaseModel, ConfigDict, Field, field_validato"
  },
  {
    "path": "api/models_service.py",
    "chars": 4405,
    "preview": "\"\"\"\nModels service layer using API.\n\"\"\"\n\nfrom typing import List, Optional\n\nfrom loguru import logger\n\nfrom api.client i"
  },
  {
    "path": "api/notebook_service.py",
    "chars": 2977,
    "preview": "\"\"\"\nNotebook service layer using API.\n\"\"\"\n\nfrom typing import List, Optional\n\nfrom loguru import logger\n\nfrom api.client"
  },
  {
    "path": "api/notes_service.py",
    "chars": 3195,
    "preview": "\"\"\"\nNotes service layer using API.\n\"\"\"\n\nfrom typing import List, Optional\n\nfrom loguru import logger\n\nfrom api.client im"
  },
  {
    "path": "api/podcast_api_service.py",
    "chars": 4417,
    "preview": "\"\"\"\nPodcast service layer using API client.\nThis replaces direct httpx calls in the Streamlit pages.\n\"\"\"\n\nfrom typing im"
  },
  {
    "path": "api/podcast_service.py",
    "chars": 7904,
    "preview": "from typing import Any, Dict, Optional\n\nfrom fastapi import HTTPException\nfrom loguru import logger\nfrom pydantic import"
  },
  {
    "path": "api/routers/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "api/routers/auth.py",
    "chars": 726,
    "preview": "\"\"\"\nAuthentication router for Open Notebook API.\nProvides endpoints to check authentication status.\n\"\"\"\n\nfrom fastapi im"
  },
  {
    "path": "api/routers/chat.py",
    "chars": 20073,
    "preview": "import asyncio\nimport traceback\nfrom typing import Any, Dict, List, Optional\n\nfrom fastapi import APIRouter, HTTPExcepti"
  },
  {
    "path": "api/routers/commands.py",
    "chars": 5277,
    "preview": "from typing import Any, Dict, List, Optional\n\nfrom fastapi import APIRouter, HTTPException, Query\nfrom loguru import log"
  },
  {
    "path": "api/routers/config.py",
    "chars": 5262,
    "preview": "import asyncio\nimport os\nimport time\nimport tomllib\nfrom pathlib import Path\nfrom typing import Optional\n\nfrom fastapi i"
  },
  {
    "path": "api/routers/context.py",
    "chars": 4845,
    "preview": "from fastapi import APIRouter, HTTPException\nfrom loguru import logger\n\nfrom api.models import ContextRequest, ContextRe"
  },
  {
    "path": "api/routers/credentials.py",
    "chars": 14208,
    "preview": "\"\"\"\nCredentials Router\n\nThin HTTP layer for managing individual AI provider credentials.\nBusiness logic lives in api.cre"
  },
  {
    "path": "api/routers/embedding.py",
    "chars": 4263,
    "preview": "from fastapi import APIRouter, HTTPException\nfrom loguru import logger\n\nfrom api.command_service import CommandService\nf"
  },
  {
    "path": "api/routers/embedding_rebuild.py",
    "chars": 7052,
    "preview": "from fastapi import APIRouter, HTTPException\nfrom loguru import logger\nfrom surreal_commands import get_command_status\n\n"
  },
  {
    "path": "api/routers/episode_profiles.py",
    "chars": 8267,
    "preview": "from typing import List, Optional\n\nfrom fastapi import APIRouter, HTTPException\nfrom loguru import logger\nfrom pydantic "
  },
  {
    "path": "api/routers/insights.py",
    "chars": 2896,
    "preview": "from fastapi import APIRouter, HTTPException\nfrom loguru import logger\n\nfrom api.models import NoteResponse, SaveAsNoteR"
  },
  {
    "path": "api/routers/languages.py",
    "chars": 2244,
    "preview": "from typing import List\n\nimport pycountry\nfrom babel import Locale\nfrom babel.core import get_global\nfrom fastapi import"
  },
  {
    "path": "api/routers/models.py",
    "chars": 28678,
    "preview": "import os\nimport traceback\nfrom typing import Dict, List, Optional\n\nfrom esperanto import AIFactory\nfrom fastapi import "
  },
  {
    "path": "api/routers/notebooks.py",
    "chars": 11684,
    "preview": "from typing import List, Optional\n\nfrom fastapi import APIRouter, HTTPException, Query\nfrom loguru import logger\n\nfrom a"
  },
  {
    "path": "api/routers/notes.py",
    "chars": 6688,
    "preview": "from typing import List, Literal, Optional\n\nfrom fastapi import APIRouter, HTTPException, Query\nfrom loguru import logge"
  },
  {
    "path": "api/routers/podcasts.py",
    "chars": 10614,
    "preview": "from pathlib import Path\nfrom typing import List, Optional\nfrom urllib.parse import unquote, urlparse\n\nfrom fastapi impo"
  },
  {
    "path": "api/routers/search.py",
    "chars": 8614,
    "preview": "import json\nfrom typing import AsyncGenerator\n\nfrom fastapi import APIRouter, HTTPException\nfrom fastapi.responses impor"
  },
  {
    "path": "api/routers/settings.py",
    "chars": 3685,
    "preview": "from fastapi import APIRouter, HTTPException\nfrom loguru import logger\n\nfrom api.models import SettingsResponse, Setting"
  },
  {
    "path": "api/routers/source_chat.py",
    "chars": 21123,
    "preview": "import asyncio\nimport json\nfrom typing import AsyncGenerator, List, Optional\n\nfrom fastapi import APIRouter, HTTPExcepti"
  },
  {
    "path": "api/routers/sources.py",
    "chars": 39352,
    "preview": "import asyncio\nimport os\nfrom pathlib import Path\nfrom typing import Any, List, Optional\n\nfrom fastapi import (\n    APIR"
  },
  {
    "path": "api/routers/speaker_profiles.py",
    "chars": 6222,
    "preview": "from typing import Any, Dict, List, Optional\n\nfrom fastapi import APIRouter, HTTPException\nfrom loguru import logger\nfro"
  },
  {
    "path": "api/routers/transformations.py",
    "chars": 9626,
    "preview": "from typing import List\n\nfrom fastapi import APIRouter, HTTPException\nfrom loguru import logger\n\nfrom api.models import "
  },
  {
    "path": "api/search_service.py",
    "chars": 1518,
    "preview": "\"\"\"\nSearch service layer using API.\n\"\"\"\n\nfrom typing import Any, Dict, List, Union\n\nfrom loguru import logger\n\nfrom api."
  },
  {
    "path": "api/settings_service.py",
    "chars": 2837,
    "preview": "\"\"\"\nSettings service layer using API.\n\"\"\"\n\nfrom loguru import logger\n\nfrom api.client import api_client\nfrom open_notebo"
  },
  {
    "path": "api/sources_service.py",
    "chars": 10891,
    "preview": "\"\"\"\nSources service layer using API.\n\"\"\"\n\nfrom dataclasses import dataclass\nfrom typing import Dict, List, Optional, Uni"
  },
  {
    "path": "api/transformations_service.py",
    "chars": 5275,
    "preview": "\"\"\"\nTransformations service layer using API.\n\"\"\"\n\nfrom datetime import datetime\nfrom typing import Any, Dict, List, Unio"
  },
  {
    "path": "commands/CLAUDE.md",
    "chars": 5844,
    "preview": "# Commands Module\n\n**Purpose**: Defines async command handlers for long-running operations via `surreal-commands` job qu"
  },
  {
    "path": "commands/__init__.py",
    "chars": 675,
    "preview": "\"\"\"Surreal-commands integration for Open Notebook\"\"\"\n\nfrom .embedding_commands import (\n    embed_insight_command,\n    e"
  },
  {
    "path": "commands/embedding_commands.py",
    "chars": 27319,
    "preview": "import time\nfrom typing import Dict, List, Literal, Optional\n\nfrom loguru import logger\nfrom pydantic import BaseModel\nf"
  },
  {
    "path": "commands/example_commands.py",
    "chars": 4430,
    "preview": "import asyncio\nimport time\nfrom typing import List, Optional\n\nfrom loguru import logger\nfrom pydantic import BaseModel\nf"
  },
  {
    "path": "commands/podcast_commands.py",
    "chars": 11835,
    "preview": "import time\nimport uuid\nfrom pathlib import Path\nfrom typing import Optional\n\nfrom loguru import logger\nfrom pydantic im"
  },
  {
    "path": "commands/source_commands.py",
    "chars": 9524,
    "preview": "import time\nfrom typing import Any, Dict, List, Optional\n\nfrom loguru import logger\nfrom pydantic import BaseModel\nfrom "
  },
  {
    "path": "docker-compose.yml",
    "chars": 1065,
    "preview": "services:\n  surrealdb:\n    image: surrealdb/surrealdb:v2\n    command: start --log info --user root --pass root rocksdb:/"
  },
  {
    "path": "docs/0-START-HERE/index.md",
    "chars": 1936,
    "preview": "# Open Notebook - Start Here\n\n**Open Notebook** is a privacy-focused AI research assistant. Upload documents, chat with "
  },
  {
    "path": "docs/0-START-HERE/quick-start-cloud.md",
    "chars": 5447,
    "preview": "# Quick Start - Cloud AI Providers (5 minutes)\n\nGet Open Notebook running with **Anthropic, Google, Groq, or other cloud"
  },
  {
    "path": "docs/0-START-HERE/quick-start-local.md",
    "chars": 7475,
    "preview": "# Quick Start - Local & Private (5 minutes)\n\nGet Open Notebook running with **100% local AI** using Ollama. No cloud API"
  },
  {
    "path": "docs/0-START-HERE/quick-start-openai.md",
    "chars": 4398,
    "preview": "# Quick Start - OpenAI (5 minutes)\n\nGet Open Notebook running with OpenAI's GPT models. Fast, powerful, and simple.\n\n## "
  },
  {
    "path": "docs/1-INSTALLATION/docker-compose.md",
    "chars": 8431,
    "preview": "# Docker Compose Installation (Recommended)\n\nMulti-container setup with separate services. **Best for most users.**\n\n> *"
  },
  {
    "path": "docs/1-INSTALLATION/from-source.md",
    "chars": 3625,
    "preview": "# From Source Installation\n\nClone the repository and run locally. **For developers and contributors.**\n\n## Prerequisites"
  },
  {
    "path": "docs/1-INSTALLATION/index.md",
    "chars": 4318,
    "preview": "# Installation Guide\n\nChoose your installation route based on your setup and use case.\n\n## Quick Decision: Which Route?\n"
  },
  {
    "path": "docs/1-INSTALLATION/single-container.md",
    "chars": 4381,
    "preview": "# Single Container Installation\n\nAll-in-one container setup. **Simpler than Docker Compose, but less flexible.**\n\n**Best"
  },
  {
    "path": "docs/2-CORE-CONCEPTS/ai-context-rag.md",
    "chars": 12662,
    "preview": "# AI Context & RAG - How Open Notebook Uses Your Research\n\nOpen Notebook uses different approaches to make AI models awa"
  },
  {
    "path": "docs/2-CORE-CONCEPTS/chat-vs-transformations.md",
    "chars": 10355,
    "preview": "# Chat vs. Ask vs. Transformations - Which Tool for Which Job?\n\nOpen Notebook offers different ways to work with your re"
  },
  {
    "path": "docs/2-CORE-CONCEPTS/index.md",
    "chars": 3292,
    "preview": "# Core Concepts - Understand the Mental Model\n\nBefore diving into how to use Open Notebook, it's important to understand"
  },
  {
    "path": "docs/2-CORE-CONCEPTS/notebooks-sources-notes.md",
    "chars": 9395,
    "preview": "# Notebooks, Sources, and Notes - The Container Model\n\nOpen Notebook organizes research in three connected layers. Under"
  },
  {
    "path": "docs/2-CORE-CONCEPTS/podcasts-explained.md",
    "chars": 12352,
    "preview": "# Podcasts Explained - Research as Audio Dialogue\n\nPodcasts are Open Notebook's highest-level transformation: converting"
  },
  {
    "path": "docs/3-USER-GUIDE/adding-sources.md",
    "chars": 11091,
    "preview": "# Adding Sources - Getting Content Into Your Notebook\n\nSources are the raw materials of your research. This guide covers"
  },
  {
    "path": "docs/3-USER-GUIDE/api-configuration.md",
    "chars": 11251,
    "preview": "# API Configuration\n\nConfigure AI provider credentials through the Settings UI. No file editing required.\n\n> **Credentia"
  },
  {
    "path": "docs/3-USER-GUIDE/chat-effectively.md",
    "chars": 13176,
    "preview": "# Chat Effectively - Conversations with Your Research\n\nChat is your main tool for exploratory questions and back-and-for"
  },
  {
    "path": "docs/3-USER-GUIDE/citations.md",
    "chars": 6513,
    "preview": "# Citations - Verify and Trust AI Responses\n\nCitations connect AI responses to your source materials. This guide covers "
  },
  {
    "path": "docs/3-USER-GUIDE/creating-podcasts.md",
    "chars": 14571,
    "preview": "# Creating Podcasts - Turn Research into Audio\n\nPodcasts let you consume your research passively. This guide covers the "
  },
  {
    "path": "docs/3-USER-GUIDE/index.md",
    "chars": 6574,
    "preview": "# User Guide - How to Use Open Notebook\n\nThis guide covers practical, step-by-step usage of Open Notebook features. You "
  },
  {
    "path": "docs/3-USER-GUIDE/interface-overview.md",
    "chars": 9135,
    "preview": "# Interface Overview - Finding Your Way Around\n\nOpen Notebook uses a clean three-panel layout. This guide shows you wher"
  },
  {
    "path": "docs/3-USER-GUIDE/search.md",
    "chars": 12351,
    "preview": "# Search Effectively - Finding What You Need\n\nSearch is your gateway into your research. This guide covers two search mo"
  },
  {
    "path": "docs/3-USER-GUIDE/transformations.md",
    "chars": 8833,
    "preview": "# Transformations - Batch Processing Your Sources\n\nTransformations apply the same analysis to multiple sources at once. "
  },
  {
    "path": "docs/3-USER-GUIDE/working-with-notes.md",
    "chars": 12588,
    "preview": "# Working with Notes - Capturing and Organizing Insights\n\nNotes are your processed knowledge. This guide covers how to c"
  },
  {
    "path": "docs/4-AI-PROVIDERS/index.md",
    "chars": 5664,
    "preview": "# AI Providers - Comparison & Selection Guide\n\nOpen Notebook supports 15+ AI providers. This guide helps you **choose th"
  },
  {
    "path": "docs/5-CONFIGURATION/advanced.md",
    "chars": 9953,
    "preview": "# Advanced Configuration\n\nPerformance tuning, debugging, and advanced features.\n\n---\n\n## Performance Tuning\n\n### Concurr"
  },
  {
    "path": "docs/5-CONFIGURATION/ai-providers.md",
    "chars": 14079,
    "preview": "# AI Providers - Configuration Guide\n\nComplete setup instructions for each AI provider via the **Settings UI**.\n\n> **New"
  },
  {
    "path": "docs/5-CONFIGURATION/database.md",
    "chars": 1659,
    "preview": "# Database - SurrealDB Configuration\n\nOpen Notebook uses SurrealDB for its database needs. \n\n---\n\n## Default Configurati"
  },
  {
    "path": "docs/5-CONFIGURATION/environment-reference.md",
    "chars": 11295,
    "preview": "# Complete Environment Reference\n\nComprehensive list of all environment variables available in Open Notebook.\n\n---\n\n## A"
  },
  {
    "path": "docs/5-CONFIGURATION/index.md",
    "chars": 8363,
    "preview": "# Configuration - Essential Settings\n\nConfiguration is how you customize Open Notebook for your specific setup. This sec"
  },
  {
    "path": "docs/5-CONFIGURATION/local-stt.md",
    "chars": 9490,
    "preview": "# Local Speech-to-Text Setup\n\nRun speech-to-text locally for free, private audio/video transcription using OpenAI-compat"
  },
  {
    "path": "docs/5-CONFIGURATION/local-tts.md",
    "chars": 7922,
    "preview": "# Local Text-to-Speech Setup\n\nRun text-to-speech locally for free, private podcast generation using OpenAI-compatible TT"
  },
  {
    "path": "docs/5-CONFIGURATION/mcp-integration.md",
    "chars": 5487,
    "preview": "# Model Context Protocol (MCP) Integration\n\nOpen Notebook can be seamlessly integrated into your AI workflows using the "
  },
  {
    "path": "docs/5-CONFIGURATION/ollama.md",
    "chars": 19997,
    "preview": "# Ollama Setup Guide\n\nOllama provides free, local AI models that run on your own hardware. This guide covers everything "
  },
  {
    "path": "docs/5-CONFIGURATION/openai-compatible.md",
    "chars": 9518,
    "preview": "# OpenAI-Compatible Providers\n\nUse any server that implements the OpenAI API format with Open Notebook. This includes LM"
  },
  {
    "path": "docs/5-CONFIGURATION/reverse-proxy.md",
    "chars": 24242,
    "preview": "# Reverse Proxy Configuration\n\nDeploy Open Notebook behind nginx, Caddy, Traefik, or other reverse proxies with custom d"
  },
  {
    "path": "docs/5-CONFIGURATION/security.md",
    "chars": 9709,
    "preview": "# Security Configuration\n\nProtect your Open Notebook deployment with password authentication and production hardening.\n\n"
  },
  {
    "path": "docs/6-TROUBLESHOOTING/ai-chat-issues.md",
    "chars": 9636,
    "preview": "# AI & Chat Issues - Model Configuration & Quality\n\nProblems with AI models, chat, and response quality.\n\n> **Note:** Op"
  },
  {
    "path": "docs/6-TROUBLESHOOTING/connection-issues.md",
    "chars": 8031,
    "preview": "# Connection Issues - Network & API Problems\n\nFrontend can't reach API or services won't communicate.\n\n---\n\n## \"Cannot c"
  },
  {
    "path": "docs/6-TROUBLESHOOTING/faq.md",
    "chars": 7061,
    "preview": "# Frequently Asked Questions\n\nCommon questions about Open Notebook usage, configuration, and best practices.\n\n---\n\n## Ge"
  },
  {
    "path": "docs/6-TROUBLESHOOTING/index.md",
    "chars": 7279,
    "preview": "# Troubleshooting - Problem Solving Guide\n\nHaving issues? Use this guide to diagnose and fix problems.\n\n---\n\n## How to U"
  },
  {
    "path": "docs/6-TROUBLESHOOTING/quick-fixes.md",
    "chars": 8525,
    "preview": "# Quick Fixes - Top 11 Issues & Solutions\n\nCommon problems with 1-minute solutions.\n\n---\n\n## #1: \"Cannot connect to serv"
  },
  {
    "path": "docs/7-DEVELOPMENT/api-reference.md",
    "chars": 6823,
    "preview": "# API Reference\n\nComplete REST API for Open Notebook. All endpoints are served from the API backend (default: `http://lo"
  },
  {
    "path": "docs/7-DEVELOPMENT/architecture.md",
    "chars": 27148,
    "preview": "# Open Notebook Architecture\n\n## High-Level Overview\n\nOpen Notebook follows a three-tier architecture with clear separat"
  },
  {
    "path": "docs/7-DEVELOPMENT/code-standards.md",
    "chars": 10087,
    "preview": "# Code Standards\n\nThis document outlines coding standards and best practices for Open Notebook contributions. All code s"
  },
  {
    "path": "docs/7-DEVELOPMENT/contributing.md",
    "chars": 7182,
    "preview": "# Contributing to Open Notebook\n\nThank you for your interest in contributing to Open Notebook! We welcome contributions "
  },
  {
    "path": "docs/7-DEVELOPMENT/design-principles.md",
    "chars": 11175,
    "preview": "# Design Principles & Project Vision\n\nThis document outlines the core principles, vision, and design philosophy that gui"
  },
  {
    "path": "docs/7-DEVELOPMENT/development-setup.md",
    "chars": 9270,
    "preview": "# Local Development Setup\n\nThis guide walks you through setting up Open Notebook for local development. Follow these ste"
  },
  {
    "path": "docs/7-DEVELOPMENT/index.md",
    "chars": 3419,
    "preview": "# Development\n\nWelcome to the Open Notebook development documentation! Whether you're contributing code, understanding o"
  },
  {
    "path": "docs/7-DEVELOPMENT/maintainer-guide.md",
    "chars": 12686,
    "preview": "# Maintainer Guide\n\nThis guide is for project maintainers to help manage contributions effectively while maintaining pro"
  },
  {
    "path": "docs/7-DEVELOPMENT/quick-start.md",
    "chars": 2779,
    "preview": "# Quick Start - Development\n\nGet Open Notebook running locally in 5 minutes.\n\n## Prerequisites\n\n- **Python 3.11+**\n- **G"
  },
  {
    "path": "docs/7-DEVELOPMENT/testing.md",
    "chars": 10686,
    "preview": "# Testing Guide\n\nThis document provides guidelines for writing tests in Open Notebook. Testing is critical to maintainin"
  },
  {
    "path": "docs/SECURITY_REVIEW.md",
    "chars": 3206,
    "preview": "# Security Review - API Configuration UI\n\n## Date: 2026-01-27 (Updated: 2026-01-28)\n## Reviewer: Security Audit\n\n---\n\n##"
  },
  {
    "path": "docs/index.md",
    "chars": 8625,
    "preview": "# Open Notebook Documentation\n\nWelcome to Open Notebook - a privacy-focused AI research assistant. This documentation is"
  },
  {
    "path": "examples/README.md",
    "chars": 4672,
    "preview": "# Docker Compose Examples\n\nThis folder contains different `docker-compose.yml` configurations for various use cases.\n\n##"
  },
  {
    "path": "examples/docker-compose-dev.yml",
    "chars": 600,
    "preview": "services:\n  surrealdb:\n    image: surrealdb/surrealdb:v2\n    volumes:\n      - ./surreal_data:/mydata\n    environment:\n  "
  },
  {
    "path": "examples/docker-compose-full-local.yml",
    "chars": 6027,
    "preview": "# Docker Compose - 100% Local AI Setup\n#\n# This is the complete privacy-focused setup with NO external APIs needed:\n# - "
  },
  {
    "path": "examples/docker-compose-ollama.yml",
    "chars": 1729,
    "preview": "# Docker Compose with Ollama (Free Local AI)\n#\n# This setup includes Ollama for running local AI models without API cost"
  },
  {
    "path": "examples/docker-compose-single.yml",
    "chars": 821,
    "preview": "services:\n  open_notebook_single:\n    # image: lfnovo/open_notebook:v1-latest-single\n    build:\n      context: .\n      d"
  },
  {
    "path": "examples/docker-compose-speaches.yml",
    "chars": 4123,
    "preview": "# Docker Compose with Speaches (Local TTS/STT)\n#\n# This setup includes Speaches for free, private speech processing:\n# -"
  },
  {
    "path": "frontend/.gitignore",
    "chars": 493,
    "preview": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pn"
  },
  {
    "path": "frontend/components.json",
    "chars": 430,
    "preview": "{\n  \"$schema\": \"https://ui.shadcn.com/schema.json\",\n  \"style\": \"new-york\",\n  \"rsc\": true,\n  \"tsx\": true,\n  \"tailwind\": {"
  },
  {
    "path": "frontend/eslint.config.mjs",
    "chars": 393,
    "preview": "import { dirname } from \"path\";\nimport { fileURLToPath } from \"url\";\nimport { FlatCompat } from \"@eslint/eslintrc\";\n\ncon"
  },
  {
    "path": "frontend/next.config.ts",
    "chars": 1323,
    "preview": "import type { NextConfig } from \"next\";\n\nconst nextConfig: NextConfig = {\n  // Enable standalone output for optimized Do"
  },
  {
    "path": "frontend/package.json",
    "chars": 2302,
    "preview": "{\n  \"name\": \"frontend\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"next dev\",\n    \"build\": \"nex"
  },
  {
    "path": "frontend/postcss.config.mjs",
    "chars": 81,
    "preview": "const config = {\n  plugins: [\"@tailwindcss/postcss\"],\n};\n\nexport default config;\n"
  },
  {
    "path": "frontend/src/CLAUDE.md",
    "chars": 10239,
    "preview": "# Frontend Architecture\n\nNext.js React application providing UI for Open Notebook research assistant. Three-layer archit"
  },
  {
    "path": "frontend/src/app/(auth)/login/page.tsx",
    "chars": 238,
    "preview": "import { LoginForm } from '@/components/auth/LoginForm'\nimport { ErrorBoundary } from '@/components/common/ErrorBoundary"
  },
  {
    "path": "frontend/src/app/(dashboard)/advanced/components/RebuildEmbeddings.tsx",
    "chars": 13775,
    "preview": "'use client'\n\nimport { useState, useEffect, useCallback } from 'react'\nimport { useMutation } from '@tanstack/react-quer"
  },
  {
    "path": "frontend/src/app/(dashboard)/advanced/components/SystemInfo.tsx",
    "chars": 3875,
    "preview": "'use client'\n\nimport { useEffect, useState } from 'react'\nimport { Card } from '@/components/ui/card'\nimport { getConfig"
  },
  {
    "path": "frontend/src/app/(dashboard)/advanced/page.tsx",
    "chars": 834,
    "preview": "'use client'\n\nimport { AppShell } from '@/components/layout/AppShell'\nimport { RebuildEmbeddings } from './components/Re"
  },
  {
    "path": "frontend/src/app/(dashboard)/layout.tsx",
    "chars": 1907,
    "preview": "'use client'\n\nimport { useAuth } from '@/lib/hooks/use-auth'\nimport { useVersionCheck } from '@/lib/hooks/use-version-ch"
  },
  {
    "path": "frontend/src/app/(dashboard)/notebooks/[id]/page.tsx",
    "chars": 9278,
    "preview": "'use client'\n\nimport { useState, useEffect } from 'react'\nimport { useParams } from 'next/navigation'\nimport { AppShell "
  },
  {
    "path": "frontend/src/app/(dashboard)/notebooks/components/ChatColumn.test.tsx",
    "chars": 1878,
    "preview": "import { render, screen } from '@testing-library/react'\nimport { describe, it, expect, vi } from 'vitest'\nimport { ChatC"
  },
  {
    "path": "frontend/src/app/(dashboard)/notebooks/components/ChatColumn.tsx",
    "chars": 3728,
    "preview": "'use client'\n\nimport { useMemo } from 'react'\nimport { useNotebookChat } from '@/lib/hooks/useNotebookChat'\nimport { use"
  },
  {
    "path": "frontend/src/app/(dashboard)/notebooks/components/NoteEditorDialog.tsx",
    "chars": 6760,
    "preview": "'use client'\n\nimport { Controller, useForm, useWatch } from 'react-hook-form'\nimport { useEffect, useState } from 'react"
  },
  {
    "path": "frontend/src/app/(dashboard)/notebooks/components/NotebookCard.tsx",
    "chars": 5060,
    "preview": "'use client'\n\nimport { useRouter } from 'next/navigation'\nimport { NotebookResponse } from '@/lib/types/api'\nimport { Ca"
  },
  {
    "path": "frontend/src/app/(dashboard)/notebooks/components/NotebookDeleteDialog.tsx",
    "chars": 6146,
    "preview": "'use client'\n\nimport { useState, useEffect } from 'react'\nimport {\n  AlertDialog,\n  AlertDialogAction,\n  AlertDialogCanc"
  },
  {
    "path": "frontend/src/app/(dashboard)/notebooks/components/NotebookHeader.tsx",
    "chars": 4250,
    "preview": "'use client'\n\nimport { useState } from 'react'\nimport { NotebookResponse } from '@/lib/types/api'\nimport { Button } from"
  },
  {
    "path": "frontend/src/app/(dashboard)/notebooks/components/NotebookList.tsx",
    "chars": 2434,
    "preview": "'use client'\n\nimport { NotebookResponse } from '@/lib/types/api'\nimport { NotebookCard } from './NotebookCard'\nimport { "
  },
  {
    "path": "frontend/src/app/(dashboard)/notebooks/components/NotesColumn.tsx",
    "chars": 8488,
    "preview": "'use client'\n\nimport { useState, useMemo } from 'react'\nimport { NoteResponse } from '@/lib/types/api'\nimport { Button }"
  },
  {
    "path": "frontend/src/app/(dashboard)/notebooks/components/SourcesColumn.tsx",
    "chars": 9380,
    "preview": "'use client'\n\nimport { useState, useMemo, useRef, useCallback, useEffect } from 'react'\nimport { SourceListResponse } fr"
  },
  {
    "path": "frontend/src/app/(dashboard)/notebooks/page.tsx",
    "chars": 3904,
    "preview": "'use client'\n\nimport { useMemo, useState } from 'react'\n\nimport { AppShell } from '@/components/layout/AppShell'\nimport "
  },
  {
    "path": "frontend/src/app/(dashboard)/page.tsx",
    "chars": 112,
    "preview": "import { redirect } from 'next/navigation'\n\nexport default function DashboardPage() {\n  redirect('/notebooks')\n}"
  },
  {
    "path": "frontend/src/app/(dashboard)/podcasts/page.tsx",
    "chars": 3030,
    "preview": "'use client'\n\nimport { useMemo, useState } from 'react'\nimport { AlertTriangle } from 'lucide-react'\n\nimport { AppShell "
  },
  {
    "path": "frontend/src/app/(dashboard)/search/page.tsx",
    "chars": 22331,
    "preview": "'use client'\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport { useSearchParams } from "
  },
  {
    "path": "frontend/src/app/(dashboard)/settings/api-keys/page.tsx",
    "chars": 53723,
    "preview": "'use client'\n\nimport { useMemo, useState, useEffect, useId } from 'react'\nimport { useForm } from 'react-hook-form'\nimpo"
  },
  {
    "path": "frontend/src/app/(dashboard)/settings/components/SettingsForm.tsx",
    "chars": 11713,
    "preview": "'use client'\n\nimport { useForm, Controller } from 'react-hook-form'\nimport { zodResolver } from '@hookform/resolvers/zod"
  },
  {
    "path": "frontend/src/app/(dashboard)/settings/page.tsx",
    "chars": 989,
    "preview": "'use client'\n\nimport { AppShell } from '@/components/layout/AppShell'\nimport { SettingsForm } from './components/Setting"
  },
  {
    "path": "frontend/src/app/(dashboard)/sources/[id]/page.tsx",
    "chars": 2724,
    "preview": "'use client'\n\nimport { useRouter, useParams } from 'next/navigation'\nimport { useCallback } from 'react'\nimport { Button"
  },
  {
    "path": "frontend/src/app/(dashboard)/sources/page.tsx",
    "chars": 15543,
    "preview": "'use client'\n\nimport { useState, useEffect, useCallback, useRef } from 'react'\nimport { useRouter } from 'next/navigatio"
  },
  {
    "path": "frontend/src/app/(dashboard)/transformations/components/DefaultPromptEditor.tsx",
    "chars": 3088,
    "preview": "'use client'\n\nimport { useState, useEffect, useId } from 'react'\nimport { Card, CardContent, CardDescription, CardHeader"
  },
  {
    "path": "frontend/src/app/(dashboard)/transformations/components/TransformationCard.tsx",
    "chars": 4622,
    "preview": "'use client'\n\nimport { useState } from 'react'\nimport { Card, CardContent, CardHeader } from '@/components/ui/card'\nimpo"
  },
  {
    "path": "frontend/src/app/(dashboard)/transformations/components/TransformationEditorDialog.tsx",
    "chars": 9388,
    "preview": "'use client'\n\nimport { useEffect, useId } from 'react'\nimport { Controller, useForm } from 'react-hook-form'\nimport { zo"
  },
  {
    "path": "frontend/src/app/(dashboard)/transformations/components/TransformationPlayground.tsx",
    "chars": 5905,
    "preview": "'use client'\n\nimport { useState } from 'react'\nimport { Card, CardContent, CardDescription, CardHeader, CardTitle } from"
  },
  {
    "path": "frontend/src/app/(dashboard)/transformations/components/TransformationsList.tsx",
    "chars": 2791,
    "preview": "'use client'\n\nimport { useState } from 'react'\nimport { Button } from '@/components/ui/button'\nimport { Plus } from 'luc"
  },
  {
    "path": "frontend/src/app/(dashboard)/transformations/page.tsx",
    "chars": 3253,
    "preview": "'use client'\n\nimport { useState } from 'react'\nimport { AppShell } from '@/components/layout/AppShell'\nimport { Button }"
  },
  {
    "path": "frontend/src/app/config/route.ts",
    "chars": 2500,
    "preview": "import { NextRequest, NextResponse } from 'next/server'\n\n/**\n * Runtime Configuration Endpoint\n *\n * This endpoint provi"
  },
  {
    "path": "frontend/src/app/globals.css",
    "chars": 6035,
    "preview": "@import \"tailwindcss\";\n@import \"tw-animate-css\";\n\n@theme inline {\n  --color-background: var(--background);\n  --color-for"
  },
  {
    "path": "frontend/src/app/layout.tsx",
    "chars": 1402,
    "preview": "import type { Metadata } from \"next\";\nimport { Inter } from \"next/font/google\";\nimport \"./globals.css\";\nimport { Toaster"
  },
  {
    "path": "frontend/src/app/page.tsx",
    "chars": 108,
    "preview": "import { redirect } from 'next/navigation'\n\nexport default function HomePage() {\n  redirect('/notebooks')\n}\n"
  },
  {
    "path": "frontend/src/components/auth/LoginForm.tsx",
    "chars": 6435,
    "preview": "'use client'\n\nimport { useState, useEffect } from 'react'\nimport { useRouter } from 'next/navigation'\nimport { useAuth }"
  },
  {
    "path": "frontend/src/components/common/CommandPalette.tsx",
    "chars": 10376,
    "preview": "'use client'\n\nimport { useEffect, useState, useCallback, useMemo, useId } from 'react'\nimport { useRouter } from 'next/n"
  },
  {
    "path": "frontend/src/components/common/ConfirmDialog.test.tsx",
    "chars": 1752,
    "preview": "import { describe, it, expect, vi } from 'vitest'\nimport { render, screen, fireEvent } from '@testing-library/react'\nimp"
  },
  {
    "path": "frontend/src/components/common/ConfirmDialog.tsx",
    "chars": 1809,
    "preview": "'use client'\n\nimport {\n  AlertDialog,\n  AlertDialogAction,\n  AlertDialogCancel,\n  AlertDialogContent,\n  AlertDialogDescr"
  },
  {
    "path": "frontend/src/components/common/ConnectionGuard.tsx",
    "chars": 3092,
    "preview": "'use client'\n\nimport { useEffect, useState, useCallback, useRef } from 'react'\nimport { ConnectionError } from '@/lib/ty"
  },
  {
    "path": "frontend/src/components/common/ContextIndicator.tsx",
    "chars": 3990,
    "preview": "'use client'\n\nimport { FileText, Lightbulb, StickyNote } from 'lucide-react'\nimport { Badge } from '@/components/ui/badg"
  },
  {
    "path": "frontend/src/components/common/ContextToggle.tsx",
    "chars": 2466,
    "preview": "'use client'\n\nimport { EyeOff, Lightbulb, FileText } from 'lucide-react'\nimport { Button } from '@/components/ui/button'"
  },
  {
    "path": "frontend/src/components/common/EmptyState.tsx",
    "chars": 548,
    "preview": "import { LucideIcon } from 'lucide-react'\n\ninterface EmptyStateProps {\n  icon: LucideIcon\n  title: string\n  description:"
  },
  {
    "path": "frontend/src/components/common/ErrorBoundary.tsx",
    "chars": 3473,
    "preview": "'use client'\n\nimport React from 'react'\nimport { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com"
  },
  {
    "path": "frontend/src/components/common/InlineEdit.tsx",
    "chars": 3890,
    "preview": "'use client'\n\nimport { useState, useRef, useEffect, useId, type RefObject } from 'react'\nimport { cn } from '@/lib/utils"
  },
  {
    "path": "frontend/src/components/common/LanguageLoadingOverlay.tsx",
    "chars": 2964,
    "preview": "'use client'\n\nimport { useEffect, useState, useCallback, useRef } from 'react'\nimport { useTranslation as useI18nTransla"
  },
  {
    "path": "frontend/src/components/common/LanguageToggle.tsx",
    "chars": 3267,
    "preview": "'use client'\n\nimport { Button } from '@/components/ui/button'\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  Dropdown"
  },
  {
    "path": "frontend/src/components/common/LoadingSpinner.tsx",
    "chars": 465,
    "preview": "import { Loader2 } from 'lucide-react'\nimport { cn } from '@/lib/utils'\n\ninterface LoadingSpinnerProps {\n  className?: s"
  },
  {
    "path": "frontend/src/components/common/ModelSelector.tsx",
    "chars": 2192,
    "preview": "import { useId } from 'react'\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/component"
  },
  {
    "path": "frontend/src/components/common/ThemeToggle.tsx",
    "chars": 2222,
    "preview": "'use client'\n\nimport { useTheme } from '@/lib/stores/theme-store'\nimport { Button } from '@/components/ui/button'\nimport"
  },
  {
    "path": "frontend/src/components/errors/ConnectionErrorOverlay.tsx",
    "chars": 6334,
    "preview": "'use client'\n\nimport { useState } from 'react'\nimport { Card } from '@/components/ui/card'\nimport { Button } from '@/com"
  },
  {
    "path": "frontend/src/components/layout/AppShell.tsx",
    "chars": 442,
    "preview": "'use client'\n\nimport { AppSidebar } from './AppSidebar'\nimport { SetupBanner } from './SetupBanner'\n\ninterface AppShellP"
  },
  {
    "path": "frontend/src/components/layout/AppSidebar.test.tsx",
    "chars": 2262,
    "preview": "/* eslint-disable @typescript-eslint/no-explicit-any */\nimport { render, screen, fireEvent } from '@testing-library/reac"
  },
  {
    "path": "frontend/src/components/layout/AppSidebar.tsx",
    "chars": 12803,
    "preview": "'use client'\n\nimport { useState, useEffect } from 'react'\nimport Link from 'next/link'\nimport Image from 'next/image'\nim"
  },
  {
    "path": "frontend/src/components/layout/SetupBanner.tsx",
    "chars": 3377,
    "preview": "'use client'\n\nimport { useMemo } from 'react'\nimport Link from 'next/link'\nimport { Alert, AlertTitle, AlertDescription "
  },
  {
    "path": "frontend/src/components/notebooks/CollapsibleColumn.tsx",
    "chars": 2886,
    "preview": "'use client'\n\nimport { ReactNode } from 'react'\nimport { Button } from '@/components/ui/button'\nimport { Tooltip, Toolti"
  },
  {
    "path": "frontend/src/components/notebooks/CreateNotebookDialog.tsx",
    "chars": 3252,
    "preview": "'use client'\n\nimport { useEffect } from 'react'\nimport { useForm } from 'react-hook-form'\nimport { zodResolver } from '@"
  },
  {
    "path": "frontend/src/components/podcasts/EpisodeCard.tsx",
    "chars": 17509,
    "preview": "'use client'\n\nimport { useEffect, useMemo, useState } from 'react'\nimport { formatDistanceToNow } from 'date-fns'\nimport"
  },
  {
    "path": "frontend/src/components/podcasts/EpisodeProfilesPanel.tsx",
    "chars": 11769,
    "preview": "'use client'\n\nimport { useMemo, useState } from 'react'\nimport { AlertTriangle, Copy, Edit3, MoreVertical, Trash2, Users"
  }
]

// ... and 246 more files (download for full content)

About this extraction

This page contains the full source code of the lfnovo/open-notebook GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 446 files (2.7 MB), approximately 733.7k tokens, and a symbol index with 1452 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!