Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,11 @@ release/
extension/extension.pem

# Compiled native messaging stub
backend/native_host/think-native-stub
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
51 changes: 47 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```

Expand Down
115 changes: 67 additions & 48 deletions app/electron/install-native-host.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

/**
Expand Down
File renamed without changes.
24 changes: 14 additions & 10 deletions app/scripts/notarize.js
Original file line number Diff line number Diff line change
@@ -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)');
Expand All @@ -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`,
Expand Down
15 changes: 13 additions & 2 deletions app/src/LockScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,28 +36,39 @@ 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;
}

setLoading(true);

try {
console.log('[LockScreen] Calling /api/auth/setup...');
const res = await apiFetch('/api/auth/setup', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password }),
});

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);
}
Expand Down
16 changes: 16 additions & 0 deletions backend/app/db/migrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
11 changes: 11 additions & 0 deletions backend/hooks/hook-sqlite_vec.py
Original file line number Diff line number Diff line change
@@ -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')
2 changes: 1 addition & 1 deletion backend/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
10 changes: 7 additions & 3 deletions backend/think.spec
Original file line number Diff line number Diff line change
Expand Up @@ -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':
Expand All @@ -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'],
Expand Down Expand Up @@ -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

File renamed without changes.
Loading