From 95555dddae0f33c00b4ed1aac903db6391de5405 Mon Sep 17 00:00:00 2001 From: NplusM420 Date: Tue, 16 Dec 2025 12:49:57 -0600 Subject: [PATCH 1/4] feat: add Windows support - Replace pysqlcipher3 with rotki-pysqlcipher3 (pre-built Windows wheels) - Rename postcss.config.js to .mjs (fix ESM/CJS conflict) - Add cross-platform build:stub script using Node.js - Add cross-platform build:backend script using Node.js - Add cross-platform start-backend script - Fix native host registration to use reg.exe (no winreg dependency) - Add unified dev command with concurrently - Add Windows setup script (setup-windows.ps1) - Update README with Windows instructions - Update .gitignore for Windows build artifacts All build scripts now work on Windows, macOS, and Linux. --- .gitignore | 9 +- README.md | 51 +++- app/electron/install-native-host.js | 115 ++++---- backend/pyproject.toml | 2 +- .../{postcss.config.js => postcss.config.mjs} | 0 package.json | 9 +- scripts/build-backend.js | 121 ++++++++ scripts/build-stub.js | 89 ++++++ scripts/setup-windows.ps1 | 270 ++++++++++++++++++ scripts/start-backend.js | 33 +++ 10 files changed, 642 insertions(+), 57 deletions(-) rename extension/{postcss.config.js => postcss.config.mjs} (100%) create mode 100644 scripts/build-backend.js create mode 100644 scripts/build-stub.js create mode 100644 scripts/setup-windows.ps1 create mode 100644 scripts/start-backend.js diff --git a/.gitignore b/.gitignore index cb68b61..5f72078 100644 --- a/.gitignore +++ b/.gitignore @@ -39,4 +39,11 @@ release/ extension/extension.pem # Compiled native messaging stub -backend/native_host/think-native-stub \ No newline at end of file +backend/native_host/think-native-stub +backend/native_host/think-native-stub.exe +backend/native_host/com.think.native.json +backend/native_host/com.think.native.firefox.json + +# PyInstaller +backend/build/ +backend/*.spec \ No newline at end of file diff --git a/README.md b/README.md index 1648a50..3fa8f0e 100644 --- a/README.md +++ b/README.md @@ -6,24 +6,67 @@ Personal AI assistant for saving and chatting with web content. ## Setup +### macOS / Linux + ```bash # Install dependencies pnpm install # Install backend cd backend && poetry install + +# Build native messaging stub +pnpm build:stub +``` + +### Windows + +```powershell +# Run the automated setup script +.\scripts\setup-windows.ps1 ``` +Or manually: + +```powershell +# Install dependencies +pnpm install + +# Install backend (requires Python 3.12) +cd backend +poetry env use python3.12 # if you have multiple Python versions +poetry install +cd .. + +# Build extension and native stub +pnpm ext +pnpm build:stub +``` + +**Note:** Windows requires Python 3.12 (not 3.13) for full compatibility. + ## Development +### Quick Start (All Platforms) + +```bash +# Start backend + Electron app together +pnpm dev + +# Or with extension hot-reload +pnpm dev:all +``` + +### Individual Commands + ```bash -# Terminal 1: Start backend -cd backend && poetry run uvicorn app.main:app --reload --port 8765 +# Start backend server +pnpm backend -# Terminal 2: Start Electron app +# Start Electron app pnpm app -# Terminal 3: Watch extension changes +# Watch extension changes pnpm --filter think-extension dev ``` diff --git a/app/electron/install-native-host.js b/app/electron/install-native-host.js index ff4ffc7..556cf4a 100644 --- a/app/electron/install-native-host.js +++ b/app/electron/install-native-host.js @@ -127,77 +127,96 @@ function installManifest(directory, manifest) { } } +/** + * Install Windows registry using reg.exe (no external dependencies). + */ +function installWindowsRegistryWithReg(manifestPath, regPath, browserName) { + const { execSync } = require('child_process'); + try { + // Create the registry key and set the default value to the manifest path + execSync(`reg add "${regPath}" /ve /t REG_SZ /d "${manifestPath}" /f`, { + stdio: 'pipe', + windowsHide: true + }); + console.log(`[Native Host] Installed registry entry for ${browserName}`); + return true; + } catch (error) { + console.error(`[Native Host] Failed to set registry for ${browserName}:`, error.message); + return false; + } +} + /** * Install Windows registry entries. + * Uses reg.exe as primary method (no dependencies), falls back to winreg if available. */ function installWindowsRegistry(stubPath, extensionIds) { - // Windows requires registry entries instead of manifest files - // This uses the 'winreg' module - install with: npm install winreg + // Create manifest file in app directory + const manifestDir = path.dirname(stubPath); + const chromeManifestPath = path.join(manifestDir, `${NATIVE_HOST_NAME}.json`); + const firefoxManifestPath = path.join(manifestDir, `${NATIVE_HOST_NAME}.firefox.json`); + + // Write Chrome/Edge manifest + const chromeManifest = createChromeManifest(stubPath, extensionIds); + fs.writeFileSync(chromeManifestPath, JSON.stringify(chromeManifest, null, 2)); + console.log(`[Native Host] Created manifest: ${chromeManifestPath}`); + + // Write Firefox manifest + const firefoxManifest = createFirefoxManifest(stubPath); + fs.writeFileSync(firefoxManifestPath, JSON.stringify(firefoxManifest, null, 2)); + console.log(`[Native Host] Created manifest: ${firefoxManifestPath}`); + + // Registry paths + const registryEntries = [ + { name: 'Chrome', path: `HKCU\\Software\\Google\\Chrome\\NativeMessagingHosts\\${NATIVE_HOST_NAME}`, manifest: chromeManifestPath }, + { name: 'Edge', path: `HKCU\\Software\\Microsoft\\Edge\\NativeMessagingHosts\\${NATIVE_HOST_NAME}`, manifest: chromeManifestPath }, + { name: 'Firefox', path: `HKCU\\Software\\Mozilla\\NativeMessagingHosts\\${NATIVE_HOST_NAME}`, manifest: firefoxManifestPath }, + ]; + + // Try using reg.exe first (no dependencies required) + let useRegExe = true; + try { + require('child_process').execSync('reg query HKCU /? >nul 2>&1', { stdio: 'pipe', windowsHide: true }); + } catch (e) { + useRegExe = false; + } + + if (useRegExe) { + console.log('[Native Host] Using reg.exe for registry installation...'); + for (const entry of registryEntries) { + installWindowsRegistryWithReg(entry.manifest, entry.path, entry.name); + } + return; + } + // Fallback to winreg module if reg.exe is not available let Registry; try { Registry = require('winreg'); } catch (e) { - console.error('[Native Host] winreg module not found. Install with: npm install winreg'); - console.error('[Native Host] Skipping Windows registry installation'); + console.error('[Native Host] Neither reg.exe nor winreg module available.'); + console.error('[Native Host] Registry installation failed.'); return; } - const browsers = [ - { name: 'Chrome', key: '\\Software\\Google\\Chrome\\NativeMessagingHosts\\' + NATIVE_HOST_NAME }, - { name: 'Edge', key: '\\Software\\Microsoft\\Edge\\NativeMessagingHosts\\' + NATIVE_HOST_NAME }, - ]; - - for (const browser of browsers) { + console.log('[Native Host] Using winreg module for registry installation...'); + for (const entry of registryEntries) { try { const regKey = new Registry({ hive: Registry.HKCU, - key: browser.key, + key: entry.path.replace('HKCU\\', '\\'), }); - - // Create manifest file in app directory - const manifestDir = path.dirname(stubPath); - const manifestPath = path.join(manifestDir, `${NATIVE_HOST_NAME}.json`); - - const manifest = createChromeManifest(stubPath, extensionIds); - fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2)); - - // Point registry to manifest file - regKey.set('', Registry.REG_SZ, manifestPath, (err) => { + regKey.set('', Registry.REG_SZ, entry.manifest, (err) => { if (err) { - console.error(`[Native Host] Failed to set registry for ${browser.name}:`, err); + console.error(`[Native Host] Failed to set registry for ${entry.name}:`, err); } else { - console.log(`[Native Host] Installed registry entry for ${browser.name}`); + console.log(`[Native Host] Installed registry entry for ${entry.name}`); } }); } catch (error) { - console.error(`[Native Host] Failed to install registry for ${browser.name}:`, error.message); + console.error(`[Native Host] Failed to install registry for ${entry.name}:`, error.message); } } - - // Firefox on Windows also uses registry - try { - const firefoxKey = new Registry({ - hive: Registry.HKCU, - key: '\\Software\\Mozilla\\NativeMessagingHosts\\' + NATIVE_HOST_NAME, - }); - - const manifestDir = path.dirname(stubPath); - const manifestPath = path.join(manifestDir, `${NATIVE_HOST_NAME}.firefox.json`); - - const manifest = createFirefoxManifest(stubPath); - fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2)); - - firefoxKey.set('', Registry.REG_SZ, manifestPath, (err) => { - if (err) { - console.error('[Native Host] Failed to set registry for Firefox:', err); - } else { - console.log('[Native Host] Installed registry entry for Firefox'); - } - }); - } catch (error) { - console.error('[Native Host] Failed to install registry for Firefox:', error.message); - } } /** diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 06732cc..157ef85 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -14,7 +14,7 @@ pydantic = "^2.9.0" pydantic-settings = "^2.6.0" httpx = "^0.27.0" aiosqlite = "^0.20.0" -pysqlcipher3 = "^1.2.0" +rotki-pysqlcipher3 = "^2024.10.1" sqlalchemy = {extras = ["asyncio"], version = "^2.0.0"} sqlite-vec = "^0.1.0" numpy = "^2.0.0" diff --git a/extension/postcss.config.js b/extension/postcss.config.mjs similarity index 100% rename from extension/postcss.config.js rename to extension/postcss.config.mjs diff --git a/package.json b/package.json index 3219bab..b6e0808 100644 --- a/package.json +++ b/package.json @@ -4,9 +4,11 @@ "scripts": { "app": "pnpm --filter think-app dev", "ext": "pnpm --filter think-extension build", - "backend": "cd backend && poetry run uvicorn app.main:app --reload --port 8765", - "build:backend": "export CODESIGN_IDENTITY=\"${CODESIGN_IDENTITY:-$(grep '^CODESIGN_IDENTITY=' .env.local 2>/dev/null | cut -d= -f2-)}\"; cd backend && poetry run pyinstaller think.spec --clean && [ -n \"$CODESIGN_IDENTITY\" ] && find dist/think-backend \\( -name '*.dylib' -o -name '*.so' \\) -exec codesign --force --sign \"$CODESIGN_IDENTITY\" --timestamp --options runtime {} \\; || echo 'Skipping signing (CODESIGN_IDENTITY not set)'", - "build:stub": "export CODESIGN_IDENTITY=\"${CODESIGN_IDENTITY:-$(grep '^CODESIGN_IDENTITY=' .env.local 2>/dev/null | cut -d= -f2-)}\"; cd backend/native_host && clang -O2 -o think-native-stub stub.c && [ -n \"$CODESIGN_IDENTITY\" ] && codesign --force --sign \"$CODESIGN_IDENTITY\" --timestamp --options runtime think-native-stub || codesign --sign - --force think-native-stub", + "backend": "node scripts/start-backend.js", + "dev": "concurrently -n backend,app -c blue,green \"pnpm backend\" \"pnpm app\"", + "dev:all": "concurrently -n backend,app,ext -c blue,green,yellow \"pnpm backend\" \"pnpm app\" \"pnpm --filter think-extension dev\"", + "build:stub": "node scripts/build-stub.js", + "build:backend": "node scripts/build-backend.js", "build:app": "pnpm --filter think-app build && pnpm --filter think-app electron:build", "build:app:release": "pnpm --filter think-app build && pnpm --filter think-app electron:build:release", "build:all": "pnpm build:backend && pnpm build:stub && pnpm build:app", @@ -17,6 +19,7 @@ "prepare": "husky" }, "devDependencies": { + "concurrently": "^8.2.2", "@changesets/cli": "^2.29.8", "@commitlint/cli": "^20.2.0", "@commitlint/config-conventional": "^20.2.0", diff --git a/scripts/build-backend.js b/scripts/build-backend.js new file mode 100644 index 0000000..a116464 --- /dev/null +++ b/scripts/build-backend.js @@ -0,0 +1,121 @@ +#!/usr/bin/env node +/** + * Cross-platform backend build script. + * + * - Windows: Uses PyInstaller without code signing + * - macOS: Uses PyInstaller with optional code signing + * - Linux: Uses PyInstaller without code signing + */ + +const { execSync } = require('child_process'); +const path = require('path'); +const fs = require('fs'); + +const isWindows = process.platform === 'win32'; +const isMac = process.platform === 'darwin'; +const backendDir = path.join(__dirname, '..', 'backend'); + +console.log(`[build-backend] Platform: ${process.platform}`); +console.log(`[build-backend] Backend dir: ${backendDir}`); + +// Check if think.spec exists +const specPath = path.join(backendDir, 'think.spec'); +if (!fs.existsSync(specPath)) { + console.error(`[build-backend] Error: ${specPath} not found`); + console.error('[build-backend] PyInstaller spec file is required for backend build'); + process.exit(1); +} + +// Get codesign identity for macOS +function getCodesignIdentity() { + if (!isMac) return null; + + // Check environment variable first + if (process.env.CODESIGN_IDENTITY) { + return process.env.CODESIGN_IDENTITY; + } + + // Try to read from .env.local + try { + const envLocalPath = path.join(__dirname, '..', '.env.local'); + if (fs.existsSync(envLocalPath)) { + const content = fs.readFileSync(envLocalPath, 'utf8'); + const match = content.match(/^CODESIGN_IDENTITY=(.+)$/m); + if (match) { + return match[1].trim(); + } + } + } catch (e) { + // Ignore errors reading .env.local + } + + return null; +} + +// Run PyInstaller +console.log('[build-backend] Running PyInstaller...'); +try { + execSync( + `poetry run pyinstaller think.spec --clean --noconfirm`, + { cwd: backendDir, stdio: 'inherit' } + ); +} catch (error) { + console.error('[build-backend] PyInstaller build failed:', error.message); + process.exit(1); +} + +// Code signing for macOS +if (isMac) { + const codesignIdentity = getCodesignIdentity(); + + if (codesignIdentity) { + console.log(`[build-backend] Signing with identity: ${codesignIdentity}`); + + const distDir = path.join(backendDir, 'dist', 'think-backend'); + + // Find all .dylib and .so files + function findFilesToSign(dir, files = []) { + const entries = fs.readdirSync(dir, { withFileTypes: true }); + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + findFilesToSign(fullPath, files); + } else if (entry.name.endsWith('.dylib') || entry.name.endsWith('.so')) { + files.push(fullPath); + } + } + return files; + } + + try { + const filesToSign = findFilesToSign(distDir); + console.log(`[build-backend] Found ${filesToSign.length} files to sign`); + + for (const file of filesToSign) { + console.log(`[build-backend] Signing: ${path.basename(file)}`); + execSync( + `codesign --force --sign "${codesignIdentity}" --timestamp --options runtime "${file}"`, + { stdio: 'pipe' } + ); + } + + console.log('[build-backend] Code signing complete'); + } catch (error) { + console.error('[build-backend] Code signing failed:', error.message); + console.error('[build-backend] Build completed but signing failed'); + } + } else { + console.log('[build-backend] No CODESIGN_IDENTITY found, skipping code signing'); + } +} + +// Verify output +const distPath = path.join(backendDir, 'dist', 'think-backend'); +if (fs.existsSync(distPath)) { + console.log(`[build-backend] Success! Built to: ${distPath}`); +} else { + console.error('[build-backend] Build completed but output directory not found'); + process.exit(1); +} + +console.log('[build-backend] Done!'); diff --git a/scripts/build-stub.js b/scripts/build-stub.js new file mode 100644 index 0000000..465a6b3 --- /dev/null +++ b/scripts/build-stub.js @@ -0,0 +1,89 @@ +#!/usr/bin/env node +/** + * Cross-platform native messaging stub build script. + * + * - Windows: Uses PyInstaller to build from stub_win.py + * - macOS/Linux: Uses clang to compile stub.c + */ + +const { execSync } = require('child_process'); +const path = require('path'); +const fs = require('fs'); + +const isWindows = process.platform === 'win32'; +const isMac = process.platform === 'darwin'; +const backendDir = path.join(__dirname, '..', 'backend'); +const nativeHostDir = path.join(backendDir, 'native_host'); + +console.log(`[build-stub] Platform: ${process.platform}`); +console.log(`[build-stub] Backend dir: ${backendDir}`); + +if (isWindows) { + console.log('[build-stub] Building Windows native stub with PyInstaller...'); + + try { + execSync( + 'poetry run pyinstaller --onefile --name think-native-stub --distpath native_host native_host/stub_win.py --noconfirm', + { cwd: backendDir, stdio: 'inherit' } + ); + + const exePath = path.join(nativeHostDir, 'think-native-stub.exe'); + if (fs.existsSync(exePath)) { + console.log(`[build-stub] Success! Built: ${exePath}`); + } else { + console.error('[build-stub] Build completed but exe not found'); + process.exit(1); + } + } catch (error) { + console.error('[build-stub] PyInstaller build failed:', error.message); + process.exit(1); + } +} else { + console.log('[build-stub] Building macOS/Linux native stub with clang...'); + + try { + execSync('clang -O2 -o think-native-stub stub.c', { + cwd: nativeHostDir, + stdio: 'inherit' + }); + + const stubPath = path.join(nativeHostDir, 'think-native-stub'); + + if (isMac) { + // Code signing for macOS + const codesignIdentity = process.env.CODESIGN_IDENTITY || + (() => { + try { + const envLocal = path.join(__dirname, '..', '.env.local'); + if (fs.existsSync(envLocal)) { + const content = fs.readFileSync(envLocal, 'utf8'); + const match = content.match(/^CODESIGN_IDENTITY=(.+)$/m); + return match ? match[1].trim() : null; + } + } catch (e) {} + return null; + })(); + + if (codesignIdentity) { + console.log(`[build-stub] Signing with identity: ${codesignIdentity}`); + execSync( + `codesign --force --sign "${codesignIdentity}" --timestamp --options runtime think-native-stub`, + { cwd: nativeHostDir, stdio: 'inherit' } + ); + } else { + console.log('[build-stub] No CODESIGN_IDENTITY found, using ad-hoc signing'); + execSync('codesign --sign - --force think-native-stub', { + cwd: nativeHostDir, + stdio: 'inherit' + }); + } + } + + console.log(`[build-stub] Success! Built: ${stubPath}`); + } catch (error) { + console.error('[build-stub] Build failed:', error.message); + process.exit(1); + } +} + +console.log('[build-stub] Done!'); diff --git a/scripts/setup-windows.ps1 b/scripts/setup-windows.ps1 new file mode 100644 index 0000000..32b8235 --- /dev/null +++ b/scripts/setup-windows.ps1 @@ -0,0 +1,270 @@ +#!/usr/bin/env pwsh +<# +.SYNOPSIS + ThinkOS-Client Windows Setup Script +.DESCRIPTION + Automated setup script for ThinkOS-Client on Windows. + Installs dependencies, builds the extension and native stub, + and registers the native messaging host. +.NOTES + Requires: Windows 10/11, Python 3.12, Node.js 18+, pnpm +#> + +param( + [switch]$SkipPython, + [switch]$SkipNode, + [switch]$SkipBuild, + [switch]$Help +) + +if ($Help) { + Write-Host @" +ThinkOS-Client Windows Setup Script + +Usage: .\scripts\setup-windows.ps1 [options] + +Options: + -SkipPython Skip Python version check and installation + -SkipNode Skip Node.js dependency installation + -SkipBuild Skip building extension and native stub + -Help Show this help message + +"@ + exit 0 +} + +$ErrorActionPreference = "Stop" + +function Write-Step { + param([string]$Message) + Write-Host "`n=== $Message ===" -ForegroundColor Cyan +} + +function Write-Success { + param([string]$Message) + Write-Host "[OK] $Message" -ForegroundColor Green +} + +function Write-Warning { + param([string]$Message) + Write-Host "[WARN] $Message" -ForegroundColor Yellow +} + +function Write-Error { + param([string]$Message) + Write-Host "[ERROR] $Message" -ForegroundColor Red +} + +# Header +Write-Host @" + + _____ _ _ _ ___ ____ + |_ _| |__ (_)_ __ | | __/ _ \/ ___| + | | | '_ \| | '_ \| |/ / | | \___ \ + | | | | | | | | | | <| |_| |___) | + |_| |_| |_|_|_| |_|_|\_\\___/|____/ + + Windows Setup Script + +"@ -ForegroundColor Magenta + +# Check if running from project root +if (-not (Test-Path "package.json")) { + Write-Error "Please run this script from the project root directory" + exit 1 +} + +# Step 1: Check Python +if (-not $SkipPython) { + Write-Step "Checking Python Installation" + + $pythonVersion = $null + try { + $pythonVersion = (python --version 2>&1) -replace "Python ", "" + } catch { + $pythonVersion = $null + } + + if ($pythonVersion -and $pythonVersion -match "^3\.12") { + Write-Success "Python $pythonVersion detected" + } elseif ($pythonVersion -and $pythonVersion -match "^3\.13") { + Write-Warning "Python 3.13 detected - this may have compatibility issues" + Write-Host "Recommended: Install Python 3.12 for best compatibility" -ForegroundColor Yellow + + # Check if 3.12 is available via py launcher + try { + $py312 = py -3.12 --version 2>&1 + if ($py312 -match "3\.12") { + Write-Success "Python 3.12 also available via 'py -3.12'" + Write-Host "Consider using: poetry env use py -3.12" -ForegroundColor Yellow + } + } catch {} + } else { + Write-Warning "Python 3.12 not found (current: $pythonVersion)" + Write-Host "Installing Python 3.12 via winget..." -ForegroundColor Yellow + + try { + winget install Python.Python.3.12 --accept-package-agreements --accept-source-agreements + Write-Success "Python 3.12 installed" + Write-Host "Please restart your terminal and run this script again." -ForegroundColor Yellow + exit 0 + } catch { + Write-Error "Failed to install Python 3.12. Please install manually from python.org" + exit 1 + } + } + + # Check Poetry + try { + $poetryVersion = poetry --version 2>&1 + Write-Success "Poetry detected: $poetryVersion" + } catch { + Write-Warning "Poetry not found. Installing..." + try { + (Invoke-WebRequest -Uri https://install.python-poetry.org -UseBasicParsing).Content | python - + Write-Success "Poetry installed" + Write-Host "Please restart your terminal and run this script again." -ForegroundColor Yellow + exit 0 + } catch { + Write-Error "Failed to install Poetry. Please install manually: https://python-poetry.org/docs/#installation" + exit 1 + } + } +} + +# Step 2: Install Node dependencies +if (-not $SkipNode) { + Write-Step "Installing Node.js Dependencies" + + try { + pnpm --version | Out-Null + } catch { + Write-Error "pnpm not found. Please install pnpm: npm install -g pnpm" + exit 1 + } + + Write-Host "Running pnpm install..." -ForegroundColor Gray + pnpm install + Write-Success "Node.js dependencies installed" +} + +# Step 3: Install Python dependencies +Write-Step "Installing Python Dependencies" + +Push-Location backend +try { + # Try to use Python 3.12 specifically + $python312Path = $null + try { + $python312Path = (py -3.12 -c "import sys; print(sys.executable)" 2>$null) + } catch {} + + if ($python312Path) { + Write-Host "Configuring Poetry to use Python 3.12..." -ForegroundColor Gray + poetry env use $python312Path 2>$null + } + + Write-Host "Running poetry install..." -ForegroundColor Gray + poetry install + Write-Success "Python dependencies installed" +} catch { + Write-Error "Failed to install Python dependencies: $_" + Pop-Location + exit 1 +} +Pop-Location + +# Step 4: Build extension +if (-not $SkipBuild) { + Write-Step "Building Chrome Extension" + + try { + pnpm ext + Write-Success "Extension built: extension/dist/" + } catch { + Write-Error "Failed to build extension: $_" + exit 1 + } + + # Step 5: Build native stub + Write-Step "Building Native Messaging Stub" + + try { + pnpm build:stub + + $stubPath = "backend/native_host/think-native-stub.exe" + if (Test-Path $stubPath) { + Write-Success "Native stub built: $stubPath" + } else { + Write-Error "Native stub not found after build" + exit 1 + } + } catch { + Write-Error "Failed to build native stub: $_" + exit 1 + } +} + +# Step 6: Register native messaging host +Write-Step "Registering Native Messaging Host" + +$stubPath = (Resolve-Path "backend/native_host/think-native-stub.exe").Path +$manifestDir = (Resolve-Path "backend/native_host").Path +$manifestPath = Join-Path $manifestDir "com.think.native.json" + +# Create manifest +$manifest = @{ + name = "com.think.native" + description = "Think Native Messaging Host - Secure communication between Think browser extension and desktop app" + path = $stubPath + type = "stdio" + allowed_origins = @("chrome-extension://ddkjmfghdikcpfnemhpecpmiajjhghoi/") +} | ConvertTo-Json -Depth 10 + +$manifest | Out-File -FilePath $manifestPath -Encoding UTF8 -NoNewline +Write-Success "Created manifest: $manifestPath" + +# Add registry entries +$browsers = @( + @{ Name = "Chrome"; Path = "HKCU:\Software\Google\Chrome\NativeMessagingHosts\com.think.native" }, + @{ Name = "Edge"; Path = "HKCU:\Software\Microsoft\Edge\NativeMessagingHosts\com.think.native" } +) + +foreach ($browser in $browsers) { + try { + New-Item -Path $browser.Path -Force | Out-Null + Set-ItemProperty -Path $browser.Path -Name "(Default)" -Value $manifestPath + Write-Success "Registered native host for $($browser.Name)" + } catch { + Write-Warning "Failed to register for $($browser.Name): $_" + } +} + +# Done! +Write-Host @" + +=== Setup Complete! === + +"@ -ForegroundColor Green + +Write-Host @" +Next steps: + +1. Load the Chrome extension: + - Open Chrome and go to: chrome://extensions + - Enable "Developer mode" (toggle in top-right) + - Click "Load unpacked" + - Select folder: extension/dist + +2. Start the development servers: + pnpm dev + + Or start individually: + - Backend: pnpm backend + - Electron: pnpm app + +3. The extension should now connect to the backend! + +"@ -ForegroundColor White + +Write-Host "For issues, see: .ai/windows-compatibility-report.md" -ForegroundColor Gray diff --git a/scripts/start-backend.js b/scripts/start-backend.js new file mode 100644 index 0000000..94a60f7 --- /dev/null +++ b/scripts/start-backend.js @@ -0,0 +1,33 @@ +#!/usr/bin/env node +/** + * Cross-platform backend start script. + * Handles working directory properly on all platforms. + */ + +const { spawn } = require('child_process'); +const path = require('path'); + +const backendDir = path.join(__dirname, '..', 'backend'); +const isWindows = process.platform === 'win32'; + +console.log(`[start-backend] Starting backend server...`); +console.log(`[start-backend] Working directory: ${backendDir}`); + +const child = spawn( + isWindows ? 'poetry.cmd' : 'poetry', + ['run', 'uvicorn', 'app.main:app', '--reload', '--port', '8765'], + { + cwd: backendDir, + stdio: 'inherit', + shell: true + } +); + +child.on('error', (error) => { + console.error('[start-backend] Failed to start:', error.message); + process.exit(1); +}); + +child.on('exit', (code) => { + process.exit(code || 0); +}); From 9e5729a8fb798f1e1f9a2ac3f322d10a6a011596 Mon Sep 17 00:00:00 2001 From: NplusM420 Date: Tue, 16 Dec 2025 15:50:15 -0600 Subject: [PATCH 2/4] fix: additional Windows build fixes - Rename app/postcss.config.js to .mjs (ESM fix) - Fix notarize.js to skip ESM import on Windows - Use dynamic import for @electron/notarize --- app/{postcss.config.js => postcss.config.mjs} | 0 app/scripts/notarize.js | 24 +++++++++++-------- 2 files changed, 14 insertions(+), 10 deletions(-) rename app/{postcss.config.js => postcss.config.mjs} (100%) diff --git a/app/postcss.config.js b/app/postcss.config.mjs similarity index 100% rename from app/postcss.config.js rename to app/postcss.config.mjs diff --git a/app/scripts/notarize.js b/app/scripts/notarize.js index f001a3e..c1b3fbc 100644 --- a/app/scripts/notarize.js +++ b/app/scripts/notarize.js @@ -1,20 +1,21 @@ -const { notarize } = require('@electron/notarize'); const path = require('path'); const fs = require('fs'); -// Load .env.local from project root -const envPath = path.resolve(__dirname, '../../.env.local'); -if (fs.existsSync(envPath)) { - require('dotenv').config({ path: envPath }); - console.log('Loaded environment from:', envPath); -} else { - console.log('No .env.local found at:', envPath); -} - exports.default = async function notarizing(context) { const { electronPlatformName, appOutDir } = context; + + // Skip notarization on non-macOS platforms if (electronPlatformName !== 'darwin') return; + // Load .env.local from project root + const envPath = path.resolve(__dirname, '../../.env.local'); + if (fs.existsSync(envPath)) { + require('dotenv').config({ path: envPath }); + console.log('Loaded environment from:', envPath); + } else { + console.log('No .env.local found at:', envPath); + } + // Skip notarization unless NOTARIZE=1 is set if (process.env.NOTARIZE !== '1') { console.log('Skipping notarization (use pnpm build:all:release for notarized builds)'); @@ -35,6 +36,9 @@ exports.default = async function notarizing(context) { const appName = context.packager.appInfo.productFilename; + // Dynamic import to avoid ESM issues on Windows + const { notarize } = await import('@electron/notarize'); + console.log('Notarizing application...'); await notarize({ appPath: `${appOutDir}/${appName}.app`, From e28182b366bea20b45291b729425fac86c19343b Mon Sep 17 00:00:00 2001 From: NplusM420 Date: Tue, 16 Dec 2025 16:29:31 -0600 Subject: [PATCH 3/4] fix: handle missing FTS5 module on Windows - Add FTS5 availability check before creating virtual table - Skip FTS migration gracefully on Windows SQLCipher builds - Fix start-backend.js to use 'poetry' with shell:true --- backend/app/db/migrations.py | 16 ++++++++++++++++ scripts/start-backend.js | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/backend/app/db/migrations.py b/backend/app/db/migrations.py index 840fff1..eaf56ef 100644 --- a/backend/app/db/migrations.py +++ b/backend/app/db/migrations.py @@ -201,9 +201,25 @@ def migration_007(conn: Connection) -> None: """)) +def _has_fts5_support(conn: Connection) -> bool: + """Check if FTS5 module is available in this SQLite build.""" + try: + conn.execute(text("CREATE VIRTUAL TABLE _fts5_test USING fts5(test)")) + conn.execute(text("DROP TABLE _fts5_test")) + return True + except Exception: + return False + + @migration(8, "Add FTS5 full-text search for memories") def migration_008(conn: Connection) -> None: """Create FTS5 virtual table for hybrid search.""" + # Check if FTS5 is available (not all SQLite builds include it) + if not _has_fts5_support(conn): + print("Warning: FTS5 not available in this SQLite build. Skipping FTS migration.") + print("Full-text search will use fallback LIKE queries instead.") + return + result = conn.execute(text( "SELECT name FROM sqlite_master WHERE type='table' AND name='memories_fts'" )).fetchone() diff --git a/scripts/start-backend.js b/scripts/start-backend.js index 94a60f7..8907a1c 100644 --- a/scripts/start-backend.js +++ b/scripts/start-backend.js @@ -14,7 +14,7 @@ console.log(`[start-backend] Starting backend server...`); console.log(`[start-backend] Working directory: ${backendDir}`); const child = spawn( - isWindows ? 'poetry.cmd' : 'poetry', + 'poetry', ['run', 'uvicorn', 'app.main:app', '--reload', '--port', '8765'], { cwd: backendDir, From 90f674a65703a8d6a5a5abe06fd34ee9f4c8211b Mon Sep 17 00:00:00 2001 From: NplusM420 Date: Tue, 16 Dec 2025 16:56:41 -0600 Subject: [PATCH 4/4] fix: bundle sqlite_vec DLL for Windows - Update think.spec to include vec0.dll on Windows (was only including macOS dylib) - Add PyInstaller hook for sqlite_vec as backup - Add debug logging to LockScreen for password setup errors --- app/src/LockScreen.tsx | 15 +++++++++++++-- backend/hooks/hook-sqlite_vec.py | 11 +++++++++++ backend/think.spec | 10 +++++++--- 3 files changed, 31 insertions(+), 5 deletions(-) create mode 100644 backend/hooks/hook-sqlite_vec.py diff --git a/app/src/LockScreen.tsx b/app/src/LockScreen.tsx index e6622f2..33deec2 100644 --- a/app/src/LockScreen.tsx +++ b/app/src/LockScreen.tsx @@ -36,7 +36,13 @@ export default function LockScreen({ needsSetup, onUnlock }: Props) { }; const handleSetup = async () => { - if (password.length < 8 || password !== confirmPassword) { + if (password.length < 8) { + console.error('[LockScreen] Password too short:', password.length, 'chars (need 8+)'); + triggerShake(); + return; + } + if (password !== confirmPassword) { + console.error('[LockScreen] Passwords do not match'); triggerShake(); return; } @@ -44,6 +50,7 @@ export default function LockScreen({ needsSetup, onUnlock }: Props) { setLoading(true); try { + console.log('[LockScreen] Calling /api/auth/setup...'); const res = await apiFetch('/api/auth/setup', { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -51,13 +58,17 @@ export default function LockScreen({ needsSetup, onUnlock }: Props) { }); if (res.ok) { + console.log('[LockScreen] Setup successful!'); setSuccess(true); setTimeout(() => onUnlock(), 600); } else { + const errorText = await res.text(); + console.error('[LockScreen] Setup failed:', res.status, errorText); triggerShake(); setLoading(false); } - } catch { + } catch (err) { + console.error('[LockScreen] Setup error:', err); triggerShake(); setLoading(false); } diff --git a/backend/hooks/hook-sqlite_vec.py b/backend/hooks/hook-sqlite_vec.py new file mode 100644 index 0000000..b16a16b --- /dev/null +++ b/backend/hooks/hook-sqlite_vec.py @@ -0,0 +1,11 @@ +"""PyInstaller hook for sqlite_vec package. + +sqlite_vec loads a native DLL (vec0.dll on Windows, vec0.so on Linux, vec0.dylib on macOS) +that needs to be bundled with the application. +""" + +from PyInstaller.utils.hooks import collect_data_files, collect_dynamic_libs + +# Collect the native extension DLL +datas = collect_data_files('sqlite_vec') +binaries = collect_dynamic_libs('sqlite_vec') diff --git a/backend/think.spec b/backend/think.spec index d4200dc..e728a83 100644 --- a/backend/think.spec +++ b/backend/think.spec @@ -6,7 +6,10 @@ from pathlib import Path # Find sqlite-vec dylib import sqlite_vec sqlite_vec_dir = Path(sqlite_vec.__file__).parent -sqlite_vec_dylib = sqlite_vec_dir / 'vec0.dylib' +if sys.platform == 'win32': + sqlite_vec_ext = sqlite_vec_dir / 'vec0.dll' +else: + sqlite_vec_ext = sqlite_vec_dir / 'vec0.dylib' # Detect platform for SQLCipher library if sys.platform == 'darwin': @@ -24,8 +27,8 @@ else: binaries = [] if os.path.exists(sqlcipher_lib): binaries.append((sqlcipher_lib, '.')) -if sqlite_vec_dylib.exists(): - binaries.append((str(sqlite_vec_dylib), 'sqlite_vec')) +if sqlite_vec_ext.exists(): + binaries.append((str(sqlite_vec_ext), 'sqlite_vec')) a = Analysis( ['run.py'], @@ -93,3 +96,4 @@ coll = COLLECT( # Note: Native messaging stub is now compiled as pure C via 'pnpm build:stub' # This eliminates the Python.framework dependency that caused Gatekeeper issues on macOS # See: backend/native_host/stub.c +