{"id":21890064,"url":"https://github.com/ejfox/vulpecula-loom","last_synced_at":"2026-04-11T03:04:36.336Z","repository":{"id":263373846,"uuid":"890174389","full_name":"ejfox/vulpecula-loom","owner":"ejfox","description":"Desktop AI chat app with Obsidian integration, OpenRouter support, and more","archived":false,"fork":false,"pushed_at":"2025-09-08T02:12:23.000Z","size":4774,"stargazers_count":1,"open_issues_count":13,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-04-03T21:49:49.812Z","etag":null,"topics":["electron","llm","obsidian","openrouter-api"],"latest_commit_sha":null,"homepage":"https://ejfox.github.io/vulpecula-loom/","language":"Vue","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ejfox.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2024-11-18T05:45:49.000Z","updated_at":"2025-09-08T02:12:27.000Z","dependencies_parsed_at":"2024-12-08T03:23:48.910Z","dependency_job_id":"4ba5f392-0a01-4d5e-a6df-e159f963e786","html_url":"https://github.com/ejfox/vulpecula-loom","commit_stats":null,"previous_names":["ejfox/vulpecula-loom"],"tags_count":3,"template":false,"template_full_name":null,"purl":"pkg:github/ejfox/vulpecula-loom","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ejfox%2Fvulpecula-loom","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ejfox%2Fvulpecula-loom/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ejfox%2Fvulpecula-loom/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ejfox%2Fvulpecula-loom/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ejfox","download_url":"https://codeload.github.com/ejfox/vulpecula-loom/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ejfox%2Fvulpecula-loom/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31667034,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-10T17:19:37.612Z","status":"online","status_checked_at":"2026-04-11T02:00:05.776Z","response_time":54,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["electron","llm","obsidian","openrouter-api"],"created_at":"2024-11-28T11:28:33.528Z","updated_at":"2026-04-11T03:04:36.289Z","avatar_url":"https://github.com/ejfox.png","language":"Vue","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Vulpecula Loom - AI Chat with Obsidian Integration\n\n\u003cimg width=\"1428\" alt=\"Screenshot 2024-11-18 at 12 41 15 AM\" src=\"https://github.com/user-attachments/assets/d2d0cec6-872f-4384-82a1-af21b8672ee6\"\u003e\n\n\nVulpecula Loom is a desktop application that combines the power of modern AI language models with seamless Obsidian vault integration. Built with Electron, Vue 3, and TypeScript, it offers a polished and efficient interface for AI-assisted writing and research.\n\n## Features\n\n### AI Chat\n- Support for multiple AI models through OpenRouter\n  - Claude 3 (Opus \u0026 Sonnet)\n  - GPT-4\n  - GPT-3.5 Turbo\n- Real-time token counting and cost tracking\n- Markdown rendering with syntax highlighting\n- Chat history with persistent storage\n- Export conversations to Markdown\n\n### Obsidian Integration\n\nShare your knowledge with AI by referencing your notes. Simply type `@` in any message to seamlessly include content from your Obsidian vault:\n\n1. **Quick Note References**\n   ```\n   Hey AI, can you help me understand @quantum-computing-basics? \n   Also, how does it relate to @quantum-entanglement?\n   ```\n   - Type `@` anywhere in your message\n   - Search your vault in real-time\n   - Select files from the popup\n   - Multiple files can be referenced in one message\n\n2. **How It Works**\n   - When you include a note with `@`, the entire note's content is sent to the AI\n   - The AI can then reference and analyze your notes\n   - Example:\n     ```\n     User: Can you compare the concepts in @classical-computing with @quantum-computing?\n     Assistant: Looking at your notes, I can see several key differences...\n     [Assistant can now reference the full content of both notes]\n     ```\n\n3. **File Search Features**\n   - Real-time search as you type after `@`\n   - Instant results (debounced 150ms)\n   - Shows file previews\n   - Matches titles and content\n   - Keyboard navigation (↑↓ to select, Enter to choose)\n   - Results sorted by relevance:\n     1. Title matches\n     2. Content matches\n     3. Recently modified\n\n4. **Technical Implementation**\n   - Files included via `includedFiles` array in messages\n   - Each included file contains:\n     ```typescript\n     {\n       title: string;      // File title\n       path: string;       // Full path to file\n       content: string;    // Complete file content\n     }\n     ```\n   - Content is cached for performance\n   - Files monitored for changes\n   - Vault path stored securely\n\n5. **Security \u0026 Performance**\n   - Files only accessible within your vault\n   - Content sanitized before display\n   - No external file access\n   - Efficient caching system\n   - Background indexing\n   - Incremental updates\n\n1. **File Inclusion**\n   - Files can be included in messages using `@` mentions\n   - When a file is included, its entire content is added to the message\n   - Included files are tracked in the message's `includedFiles` array\n   - Each included file contains:\n     ```typescript\n     {\n       title: string;      // File title\n       path: string;       // Full path to file\n       content: string;    // Complete file content\n     }\n     ```\n\n2. **File Search**\n   - Real-time search as you type `@`\n   - Debounced queries (150ms)\n   - Results limited to 10 files\n   - Results sorted by relevance\n   - Cache management for performance\n\n3. **Security**\n   - Files are only accessible within the vault\n   - Content is sanitized before display\n   - No external file access allowed\n   - Vault path stored securely in electron-store\n\n4. **Performance**\n   - File content cached in memory\n   - Incremental search updates\n   - Debounced search queries\n   - Background file indexing\n\n### User Interface\n- Dark/Light mode support\n- Native system integration\n- Custom titlebar with gradient animation\n- Responsive design with smooth transitions\n- Keyboard shortcuts for common actions\n\n### IPC Communication\n\nAll inter-process communication (IPC) is centralized in `electron/ipc/` with the following structure:\n\n```\nelectron/ipc/\n├── index.ts          # Central IPC setup and system-level handlers\n├── obsidian.ts       # Obsidian integration handlers\n└── store.ts          # Electron store handlers\n```\n\n#### Type-Safe IPC Communication\n\nAll IPC channels are fully typed using TypeScript interfaces in `src/types.ts` under the `IpcChannels` interface. \nThis centralized type system provides:\n- Auto-completion for channel names\n- Parameter type checking\n- Return type inference\n- Compile-time validation\n- Single source of truth for all IPC types\n\nExample:\n```typescript\n// In src/types.ts\nexport interface IpcChannels {\n  \"open-external\": (url: string) =\u003e Promise\u003cvoid\u003e;\n  \"store-get\": \u003cK extends keyof StoreSchema\u003e(key: K) =\u003e Promise\u003cStoreSchema[K]\u003e;\n  // ... more channel definitions\n}\n```\n\n#### Adding New IPC Handlers\n\n1. Add the type definition to `src/types.ts` under the `IpcChannels` interface\n2. Create a new handler in the appropriate domain file in `electron/ipc/`\n3. Register the handler in `electron/ipc/index.ts`\n4. Update this documentation\n\n## Getting Started\n\n### Prerequisites\n- Node.js (v18 or higher)\n- npm or yarn\n- An OpenRouter API key\n- Obsidian vault (optional)\n\n### Building the Application\n\n#### For macOS\n\n1. `git clone https://github.com/yourusername/vulpecula-loom.git`\n2. `cd vulpecula-loom`\n3. `yarn install`\n4. `yarn build`\n\nThe built application will be available in the `release/{version}/` directory:\n- `electron-vue-vite.app` - The macOS application bundle\n- `electron-vue-vite-{version}-arm64.dmg` - The disk image installer for Apple Silicon\n- `electron-vue-vite-{version}-x64.dmg` - The disk image installer for Intel Macs\n\nNote: To build a signed application for distribution, you'll need an Apple Developer account and appropriate certificates. See [Apple's documentation](https://developer.apple.com/documentation/xcode/notarizing_macos_software_before_distribution) for more details on app notarization.\n\n## TypeScript Conventions\n\n### The Central Type System\n\nALL types MUST live in `src/types.ts`. This is not just a convention - it's a requirement.\n\n#### Why Centralization?\n- Single source of truth\n- No duplicate definitions\n- Clear type hierarchies\n- Easy to find and modify\n- Better TypeScript tooling support\n- Prevents circular dependencies\n- Maintains type consistency\n\n#### Type Categories\n- OpenRouter Types (API models, responses)\n- Chat Types (messages, history)\n- Store Types (electron store schema)\n- Electron IPC Types (channel definitions)\n- UI Types (preferences, settings)\n- Animation Types (configuration)\n\n#### The Rules of Type Centralization\n\n1. ALL types MUST be in `src/types.ts`\n2. NO `.d.ts` files allowed (except for third-party type declarations)\n3. NO interface/type definitions in component files\n4. NO scattered type definitions across the codebase\n5. ALL types MUST be properly categorized and documented\n6. ALL types MUST be exported\n\n❌ ILLEGAL:\n- Types in component files\n- Scattered `.d.ts` files\n- Duplicate type definitions\n- Undocumented types\n- Types without proper categorization\n\n✅ LEGAL:\n- All types in `src/types.ts`\n- Clear type categorization\n- Proper exports\n- Well-documented types\n- Single source of truth\n\n### Type Safety vs Pragmatism\nWhile we maintain strict type centralization, we allow `any` or more permissive types when:\n- Dealing with third-party libraries\n- Prototyping features\n- Handling complex DOM elements\n- Working with dynamic data\n\nRemember: Type centralization is about organization, not restriction.\n\n## Architecture\n\n### Composables\n\nThe application uses a layered composable architecture:\n\n- `useAIChat` - The main chat composable that handles all chat functionality. This is the primary interface that components should use for chat operations. It provides:\n  - Message handling\n  - Token tracking\n  - Cost calculation\n  - Chat persistence\n  - Streaming responses\n  - History management\n\n- `useOpenRouter` - Low-level OpenRouter API integration. **Should not be used directly by components**. Instead, use `useAIChat` which provides a higher-level interface and proper state management.\n\n- `useSupabase` - Handles chat persistence and history management\n- `useObsidianFiles` - Manages Obsidian vault integration\n- `useTheme` - Handles theme switching and persistence\n\n## ⚠️ Important Implementation Notes\n\n### Chat Implementation\n\n**ALWAYS use `useAIChat.ts` for chat functionality!** \n\nThe chat system is designed around the `useAIChat` composable, which handles:\n- Message sending and receiving\n- Chat history management\n- Token tracking\n- Cost calculation\n- Model selection\n- Temperature settings\n- Chat statistics\n\n❌ **DO NOT** try to implement chat functionality by directly using `useOpenRouter` or other lower-level composables. This will:\n- Break the chat history system\n- Cause message tracking issues\n- Prevent proper token/cost tracking\n- Summon dragons that will devour everyone\n\n`useOpenRouter` should only be used for:\n- API key management\n- Model availability checking\n- Model information\n\n## Electron Store Setup\n\nThe application uses `electron-store` for persistent storage. The store is exposed to the renderer process through a secure preload script.\n\n### Store Organization\n\n1. **Store Schema**\n   - All store keys are defined in `src/types.ts`\n   - Keys follow a consistent naming pattern (e.g., `api-key`, `theme`)\n   - Values are strongly typed\n\n2. **Access Pattern**\n   - Always use the `useStore` composable\n   - Never access `window.electron.store` directly\n   - All store operations are async\n\n3. **Key Categories**\n   - API Keys (`api-key`)\n   - UI Preferences (`theme`, `show-progress-bar`)\n   - Model Settings (`enabled-model-ids`, `pinned-models`)\n   - Application State (`remember-window-state`)\n   - Integration Settings (`obsidian-vault-path`)\n\n4. **Best Practices**\n   - Use typed keys from `StoreSchema`\n   - Handle errors gracefully\n   - Provide default values\n   - Log store operations in development\n   - Clear store data responsibly\n\n## Architecture Decisions\n\n### Type System\n\nThe application follows a strict type centralization pattern:\n\n1. **Central Type Repository**\n   - ALL types MUST live in `src/types.ts`\n   - NO scattered `.d.ts` files (except for third-party declarations)\n   - NO interface/type definitions in component files\n   - ALL types MUST be properly categorized and documented\n\n2. **Type Categories**\n   - OpenRouter Types (API models, responses)\n   - Chat Types (messages, history)\n   - Store Types (electron store schema)\n   - Electron IPC Types (channel definitions)\n   - UI Types (preferences, settings)\n   - Animation Types (configuration)\n   - Obsidian Types (file handling, search)\n\n3. **Naming Conventions**\n   - Interface names are PascalCase and descriptive (e.g., `ObsidianFile`, `ChatMessage`)\n   - Options/Config interfaces end with respective suffix (e.g., `ObsidianSearchOptions`, `AnimationConfig`)\n   - Props interfaces end with `Props` (e.g., `ChatInputProps`)\n   - Return type interfaces end with `Return` (e.g., `UseSupabaseReturn`)\n\n### Composable Architecture\n\nThe application uses a layered composable architecture:\n\n1. **Core Composables**\n   - `useAIChat` - Primary chat interface\n   - `useOpenRouter` - Low-level API integration\n   - `useSupabase` - Chat persistence\n   - `useObsidianFiles` - Vault integration\n   - `useTheme` - Theme management\n\n2. **Composable Rules**\n   - Each composable has a single responsibility\n   - Composables can depend on other composables\n   - State management handled through Vue's reactivity system\n   - Error handling at every layer\n   - Type-safe return values\n\n3. **Obsidian Integration**\n   - `useObsidianFiles` manages all Obsidian interactions\n   - File search with debounced queries\n   - Cache management for performance\n   - Type-safe IPC communication\n   - Error handling and state management\n\n### IPC Communication\n\n1. **Channel Organization**\n   - All channels defined in `IpcChannels` interface\n   - Strongly typed parameters and return values\n   - Channels grouped by domain:\n     - Store operations (`store-get`, `store-set`)\n     - File operations (`search-obsidian-files`, `get-obsidian-file-content`)\n     - System operations (`open-external`)\n\n2. **Security Model**\n   - Context isolation enabled\n   - Node integration disabled\n   - Explicit channel allowlist\n   - Type-safe preload script\n\n3. **Error Handling**\n   - All IPC calls wrapped in try/catch\n   - Errors propagated to UI\n   - Fallback values defined\n   - Error states tracked in composables\n\n### State Management\n\n1. **Store Organization**\n   - Electron store for persistent data\n   - Vue refs for component state\n   - Computed properties for derived state\n   - Watchers for side effects\n\n2. **Data Flow**\n   - Props down, events up\n   - State centralized in composables\n   - IPC communication abstracted\n   - Type-safe store operations\n\n### File Structure\n\n```\nsrc/\n├── components/        # Vue components\n├── composables/       # Vue composables\n├── electron/         # Electron main process\n│   └── ipc/         # IPC handlers\n├── lib/             # Shared utilities\n└── types.ts         # Central type definitions\n```\n\n### Best Practices\n\n1. **Type Safety**\n   - Use TypeScript's strict mode\n   - No `any` types unless absolutely necessary\n   - Proper type imports from central `types.ts`\n   - Type-safe IPC communication\n\n2. **Error Handling**\n   - Graceful degradation\n   - User-friendly error messages\n   - Proper error propagation\n   - Error state management\n\n3. **Performance**\n   - Debounced search queries\n   - File caching\n   - Lazy loading where appropriate\n   - Efficient IPC communication\n\n4. **Code Style**\n   - Clear naming conventions\n   - Consistent file structure\n   - Proper documentation\n   - Single responsibility principle\n\n## Authentication \u0026 Data Security\n\n### Overview\nThe application uses Discord OAuth for authentication, managed through Supabase Auth. This ensures secure user authentication and data isolation.\n\n### Setup Requirements\n\n#### 1. Supabase Project Setup\n1. Create a new project at supabase.com\n2. Enable Discord OAuth provider:\n   - Add Discord application credentials\n   - Configure allowed callback URLs:\n     ```\n     http://localhost:5173/auth/callback  # Development\n     https://your-domain.com/auth/callback  # Production\n     ```\n3. Configure RLS policies (see Database Schema section)\n4. Copy project credentials for environment setup\n\n#### 2. Environment Configuration\nCreate a `.env` file in the project root:\n```env\n# Supabase Configuration\nVITE_SUPABASE_URL=your_project_url\nVITE_SUPABASE_KEY=your_anon_key  # Public anon key, NOT service_role key\n\n# Discord OAuth\nVITE_DISCORD_CLIENT_ID=your_discord_client_id\n```\n\n### Database Schema\n\n#### Table Structure\n```sql\n-- Enable RLS\nalter table public.vulpeculachats enable row level security;\n\n-- Create table\ncreate table public.vulpeculachats (\n  id uuid not null default gen_random_uuid(),\n  title text null,\n  messages jsonb not null default '[]'::jsonb,\n  model text not null,\n  metadata jsonb null default '{}'::jsonb,\n  created_at timestamp with time zone not null default now(),\n  updated_at timestamp with time zone not null default now(),\n  user_id uuid null,\n  thread text null,\n  constraint vulpeculachats_pkey primary key (id),\n  constraint vulpeculachats_user_id_fkey foreign key (user_id) references auth.users (id)\n) tablespace pg_default;\n\n-- Create index for efficient chat history sorting\ncreate index if not exists idx_vulpeculachats_updated_at \n  on public.vulpeculachats using btree (updated_at) \n  tablespace pg_default;\n```\n\n#### Row Level Security (RLS) Policies\n```sql\n-- Allow users to read their own chats and any anonymous chats\ncreate policy \"Users can read their own chats\"\n  on public.vulpeculachats for select\n  using (\n    auth.uid() = user_id \n    or user_id is null -- Allow reading chats with no user_id\n  );\n\n-- Allow users to create chats, either owned or anonymous\ncreate policy \"Users can create chats\"\n  on public.vulpeculachats for insert\n  with check (\n    auth.uid() = user_id \n    or user_id is null -- Allow creating chats with no user_id\n  );\n\n-- Allow users to update their own chats and anonymous chats\ncreate policy \"Users can update their own chats\"\n  on public.vulpeculachats for update\n  using (\n    auth.uid() = user_id \n    or user_id is null -- Allow updating chats with no user_id\n  );\n\n-- Allow users to delete their own chats and anonymous chats\ncreate policy \"Users can delete their own chats\"\n  on public.vulpeculachats for delete\n  using (\n    auth.uid() = user_id \n    or user_id is null -- Allow deleting chats with no user_id\n  );\n```\n\n#### Table Fields\n**vulpeculachats**: Stores all chat sessions\n- `id`: UUID primary key, auto-generated\n- `title`: Optional chat title\n- `messages`: JSONB array of messages (defaults to empty array)\n  ```typescript\n  {\n    id: string;\n    role: \"user\" | \"assistant\" | \"system\";\n    content: string;\n    timestamp: string;\n    model?: string;\n    tokens?: {\n      prompt: number;\n      completion: number;\n      total: number;\n    };\n    cost?: number;\n    includedFiles?: Array\u003c{\n      path: string;\n      content: string;\n      type: string;\n    }\u003e;\n  }[]\n  ```\n- `model`: AI model identifier (e.g., \"anthropic/claude-3-opus-20240229\")\n- `metadata`: Optional JSONB object for additional data (defaults to empty object)\n  ```typescript\n  {\n    lastModel?: string;\n    lastUpdated?: string;\n    messageCount?: number;\n    summary?: string;\n    autoTitle?: string;\n    summaryLastUpdated?: string;\n    stats?: {\n      promptTokens: number;\n      completionTokens: number;\n      cost: number;\n      totalMessages: number;\n    };\n  }\n  ```\n- `user_id`: Optional reference to auth.users (nullable for anonymous chats)\n- `thread`: Optional text field for thread identification\n- `created_at/updated_at`: Timestamps with timezone (auto-managed)\n\n#### Security Features\n- Row Level Security (RLS) enabled by default\n- Index on updated_at for efficient chat history queries\n- Full CRUD policies supporting:\n  1. User-owned chats (where user_id = auth.uid())\n  2. Anonymous chats (where user_id is null)\n  3. Proper data isolation between users\n\n#### Implementation Notes\n- Anonymous chats (null user_id) are accessible to all users\n- Each user can only access their own chats plus anonymous chats\n- The updated_at index supports efficient chat history sorting\n- JSONB fields (messages, metadata) support flexible schema evolution\n- Timestamps are automatically managed by Postgres\n\n### Authentication Flow\n1. User visits app\n2. `useActiveUser` composable checks auth state\n3. If not authenticated:\n   - Shows Discord login option\n   - Handles OAuth redirect\n   - Manages session storage\n4. On successful auth:\n   - Session established\n   - User data loaded\n   - Chat history retrieved\n\n### Logging Philosophy\n\nThe application follows a minimalist logging approach:\n\n1. **Production Logging**\n   - Critical errors only\n   - Authentication state changes\n   - Important system events\n   - No debug logs in production\n\n2. **Development Logging**\n   - Controlled through `logger.ts`\n   - Debug logs only shown in development\n   - Singleton pattern for consistent logging\n   - No redundant initialization logs\n\n3. **Log Categories**\n   ```typescript\n   export type LogLevel = \"debug\" | \"info\" | \"warn\" | \"error\";\n   ```\n   - `debug`: Development-only diagnostic information\n   - `info`: Important but non-critical system events\n   - `warn`: Potential issues that don't stop execution\n   - `error`: Critical issues that need attention\n\n4. **Implementation**\n   - Centralized logger in `src/lib/logger.ts`\n   - Singleton patterns in key composables\n   - No console.log in production code\n   - Error tracking preserved for debugging\n\n5. **Best Practices**\n   - Use `logger.error` for exceptions\n   - Keep debug logs minimal\n   - No sensitive data in logs\n   - Clear, actionable error messages","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fejfox%2Fvulpecula-loom","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fejfox%2Fvulpecula-loom","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fejfox%2Fvulpecula-loom/lists"}