fix dependency

This commit is contained in:
jhyang0 2025-07-07 16:38:37 +09:00
parent 86e903ec4b
commit 427be2a293
13 changed files with 477 additions and 659 deletions

1
.npmrc
View File

@ -1,3 +1,2 @@
better-sqlite3:ignore-scripts=true
electron-deeplink:ignore-scripts=true
sharp:ignore-scripts=true

View File

@ -10,6 +10,7 @@
"package": "npm run build:renderer && electron-forge package",
"make": "npm run build:renderer && electron-forge make",
"build": "npm run build:renderer && electron-builder --config electron-builder.yml --publish never",
"build:win": "npm run build:renderer && electron-builder --win --x64 --publish never",
"publish": "npm run build:renderer && electron-builder --config electron-builder.yml --publish always",
"lint": "eslint --ext .ts,.tsx,.js .",
"postinstall": "electron-builder install-app-deps",
@ -35,7 +36,7 @@
"better-sqlite3": "^9.4.3",
"cors": "^2.8.5",
"dotenv": "^17.0.0",
"electron-deeplink": "^1.0.10",
"electron-squirrel-startup": "^1.0.1",
"electron-store": "^8.2.0",
"electron-updater": "^6.6.2",
@ -47,7 +48,6 @@
"openai": "^4.70.0",
"react-hot-toast": "^2.5.2",
"sharp": "^0.34.2",
"sqlite3": "^5.1.7",
"validator": "^13.11.0",
"wait-on": "^8.0.3",
"ws": "^8.18.0"
@ -69,8 +69,6 @@
"esbuild": "^0.25.5"
},
"optionalDependencies": {
"@img/sharp-darwin-x64": "^0.34.2",
"@img/sharp-libvips-darwin-x64": "^1.1.0",
"electron-liquid-glass": "^1.0.1"
}
}

View File

@ -2,40 +2,34 @@ const sqliteClient = require('../../services/sqliteClient');
function getPresets(uid) {
const db = sqliteClient.getDb();
return new Promise((resolve, reject) => {
const query = `
SELECT * FROM prompt_presets
WHERE uid = ? OR is_default = 1
ORDER BY is_default DESC, title ASC
`;
db.all(query, [uid], (err, rows) => {
if (err) {
try {
return db.prepare(query).all(uid);
} catch (err) {
console.error('SQLite: Failed to get presets:', err);
reject(err);
} else {
resolve(rows);
throw err;
}
});
});
}
function getPresetTemplates() {
const db = sqliteClient.getDb();
return new Promise((resolve, reject) => {
const query = `
SELECT * FROM prompt_presets
WHERE is_default = 1
ORDER BY title ASC
`;
db.all(query, [], (err, rows) => {
if (err) {
try {
return db.prepare(query).all();
} catch (err) {
console.error('SQLite: Failed to get preset templates:', err);
reject(err);
} else {
resolve(rows);
throw err;
}
});
});
}
function create({ uid, title, prompt }) {
@ -44,38 +38,42 @@ function create({ uid, title, prompt }) {
const now = Math.floor(Date.now() / 1000);
const query = `INSERT INTO prompt_presets (id, uid, title, prompt, is_default, created_at, sync_state) VALUES (?, ?, ?, ?, 0, ?, 'dirty')`;
return new Promise((resolve, reject) => {
db.run(query, [presetId, uid, title, prompt, now], function(err) {
if (err) reject(err);
else resolve({ id: presetId });
});
});
try {
db.prepare(query).run(presetId, uid, title, prompt, now);
return { id: presetId };
} catch (err) {
throw err;
}
}
function update(id, { title, prompt }, uid) {
const db = sqliteClient.getDb();
const query = `UPDATE prompt_presets SET title = ?, prompt = ?, sync_state = 'dirty' WHERE id = ? AND uid = ? AND is_default = 0`;
return new Promise((resolve, reject) => {
db.run(query, [title, prompt, id, uid], function(err) {
if (err) reject(err);
else if (this.changes === 0) reject(new Error("Preset not found or permission denied."));
else resolve({ changes: this.changes });
});
});
try {
const result = db.prepare(query).run(title, prompt, id, uid);
if (result.changes === 0) {
throw new Error("Preset not found or permission denied.");
}
return { changes: result.changes };
} catch (err) {
throw err;
}
}
function del(id, uid) {
const db = sqliteClient.getDb();
const query = `DELETE FROM prompt_presets WHERE id = ? AND uid = ? AND is_default = 0`;
return new Promise((resolve, reject) => {
db.run(query, [id, uid], function(err) {
if (err) reject(err);
else if (this.changes === 0) reject(new Error("Preset not found or permission denied."));
else resolve({ changes: this.changes });
});
});
try {
const result = db.prepare(query).run(id, uid);
if (result.changes === 0) {
throw new Error("Preset not found or permission denied.");
}
return { changes: result.changes };
} catch (err) {
throw err;
}
}
module.exports = {

View File

@ -2,124 +2,79 @@ const sqliteClient = require('../../services/sqliteClient');
function getById(id) {
const db = sqliteClient.getDb();
return new Promise((resolve, reject) => {
db.get('SELECT * FROM sessions WHERE id = ?', [id], (err, row) => {
if (err) reject(err);
else resolve(row);
});
});
return db.prepare('SELECT * FROM sessions WHERE id = ?').get(id);
}
function create(uid, type = 'ask') {
const db = sqliteClient.getDb();
return new Promise((resolve, reject) => {
const sessionId = require('crypto').randomUUID();
const now = Math.floor(Date.now() / 1000);
const query = `INSERT INTO sessions (id, uid, title, session_type, started_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)`;
db.run(query, [sessionId, uid, `Session @ ${new Date().toLocaleTimeString()}`, type, now, now], function(err) {
if (err) {
console.error('SQLite: Failed to create session:', err);
reject(err);
} else {
try {
db.prepare(query).run(sessionId, uid, `Session @ ${new Date().toLocaleTimeString()}`, type, now, now);
console.log(`SQLite: Created session ${sessionId} for user ${uid} (type: ${type})`);
resolve(sessionId);
return sessionId;
} catch (err) {
console.error('SQLite: Failed to create session:', err);
throw err;
}
});
});
}
function getAllByUserId(uid) {
const db = sqliteClient.getDb();
return new Promise((resolve, reject) => {
const query = "SELECT id, uid, title, session_type, started_at, ended_at, sync_state, updated_at FROM sessions WHERE uid = ? ORDER BY started_at DESC";
db.all(query, [uid], (err, rows) => {
if (err) reject(err);
else resolve(rows);
});
});
return db.prepare(query).all(uid);
}
function updateTitle(id, title) {
const db = sqliteClient.getDb();
return new Promise((resolve, reject) => {
db.run('UPDATE sessions SET title = ? WHERE id = ?', [title, id], function(err) {
if (err) reject(err);
else resolve({ changes: this.changes });
});
});
const result = db.prepare('UPDATE sessions SET title = ? WHERE id = ?').run(title, id);
return { changes: result.changes };
}
function deleteWithRelatedData(id) {
const db = sqliteClient.getDb();
return new Promise((resolve, reject) => {
db.serialize(() => {
db.run("BEGIN TRANSACTION;");
const queries = [
"DELETE FROM transcripts WHERE session_id = ?",
"DELETE FROM ai_messages WHERE session_id = ?",
"DELETE FROM summaries WHERE session_id = ?",
"DELETE FROM sessions WHERE id = ?"
];
queries.forEach(query => {
db.run(query, [id], (err) => {
if (err) {
db.run("ROLLBACK;");
return reject(err);
const transaction = db.transaction(() => {
db.prepare("DELETE FROM transcripts WHERE session_id = ?").run(id);
db.prepare("DELETE FROM ai_messages WHERE session_id = ?").run(id);
db.prepare("DELETE FROM summaries WHERE session_id = ?").run(id);
db.prepare("DELETE FROM sessions WHERE id = ?").run(id);
});
try {
transaction();
return { success: true };
} catch (err) {
throw err;
}
});
});
db.run("COMMIT;", (err) => {
if (err) {
db.run("ROLLBACK;");
return reject(err);
}
resolve({ success: true });
});
});
});
}
function end(id) {
const db = sqliteClient.getDb();
return new Promise((resolve, reject) => {
const now = Math.floor(Date.now() / 1000);
const query = `UPDATE sessions SET ended_at = ?, updated_at = ? WHERE id = ?`;
db.run(query, [now, now, id], function(err) {
if (err) reject(err);
else resolve({ changes: this.changes });
});
});
const result = db.prepare(query).run(now, now, id);
return { changes: result.changes };
}
function updateType(id, type) {
const db = sqliteClient.getDb();
return new Promise((resolve, reject) => {
const now = Math.floor(Date.now() / 1000);
const query = 'UPDATE sessions SET session_type = ?, updated_at = ? WHERE id = ?';
db.run(query, [type, now, id], function(err) {
if (err) {
reject(err);
} else {
resolve({ changes: this.changes });
}
});
});
const result = db.prepare(query).run(type, now, id);
return { changes: result.changes };
}
function touch(id) {
const db = sqliteClient.getDb();
return new Promise((resolve, reject) => {
const now = Math.floor(Date.now() / 1000);
const query = 'UPDATE sessions SET updated_at = ? WHERE id = ?';
db.run(query, [now, id], function(err) {
if (err) reject(err);
else resolve({ changes: this.changes });
});
});
const result = db.prepare(query).run(now, id);
return { changes: result.changes };
}
async function getOrCreateActive(uid, requestedType = 'ask') {
function getOrCreateActive(uid, requestedType = 'ask') {
const db = sqliteClient.getDb();
// 1. Look for ANY active session for the user (ended_at IS NULL).
@ -131,12 +86,7 @@ async function getOrCreateActive(uid, requestedType = 'ask') {
LIMIT 1
`;
const activeSession = await new Promise((resolve, reject) => {
db.get(findQuery, [uid], (err, row) => {
if (err) reject(err);
else resolve(row);
});
});
const activeSession = db.prepare(findQuery).get(uid);
if (activeSession) {
// An active session exists.
@ -144,12 +94,12 @@ async function getOrCreateActive(uid, requestedType = 'ask') {
// 2. Promotion Logic: If it's an 'ask' session and we need 'listen', promote it.
if (activeSession.session_type === 'ask' && requestedType === 'listen') {
await updateType(activeSession.id, 'listen');
updateType(activeSession.id, 'listen');
console.log(`[Repo] Promoted session ${activeSession.id} to 'listen' type.`);
}
// 3. Touch the session and return its ID.
await touch(activeSession.id);
touch(activeSession.id);
return activeSession.id;
} else {
// 4. No active session found, create a new one.
@ -160,19 +110,17 @@ async function getOrCreateActive(uid, requestedType = 'ask') {
function endAllActiveSessions() {
const db = sqliteClient.getDb();
return new Promise((resolve, reject) => {
const now = Math.floor(Date.now() / 1000);
const query = `UPDATE sessions SET ended_at = ?, updated_at = ? WHERE ended_at IS NULL`;
db.run(query, [now, now], function(err) {
if (err) {
try {
const result = db.prepare(query).run(now, now);
console.log(`[Repo] Ended ${result.changes} active session(s).`);
return { changes: result.changes };
} catch (err) {
console.error('SQLite: Failed to end all active sessions:', err);
reject(err);
} else {
console.log(`[Repo] Ended ${this.changes} active session(s).`);
resolve({ changes: this.changes });
throw err;
}
});
});
}
module.exports = {

View File

@ -13,66 +13,44 @@ function findOrCreate(user) {
email=excluded.email
`;
return new Promise((resolve, reject) => {
db.run(query, [uid, displayName, email, now], (err) => {
if (err) {
try {
db.prepare(query).run(uid, displayName, email, now);
return getById(uid);
} catch (err) {
console.error('SQLite: Failed to find or create user:', err);
return reject(err);
throw err;
}
getById(uid).then(resolve).catch(reject);
});
});
}
function getById(uid) {
const db = sqliteClient.getDb();
return new Promise((resolve, reject) => {
db.get('SELECT * FROM users WHERE uid = ?', [uid], (err, row) => {
if (err) reject(err);
else resolve(row);
});
});
return db.prepare('SELECT * FROM users WHERE uid = ?').get(uid);
}
function saveApiKey(apiKey, uid, provider = 'openai') {
const db = sqliteClient.getDb();
return new Promise((resolve, reject) => {
db.run(
'UPDATE users SET api_key = ?, provider = ? WHERE uid = ?',
[apiKey, provider, uid],
function(err) {
if (err) {
console.error('SQLite: Failed to save API key:', err);
reject(err);
} else {
try {
const result = db.prepare('UPDATE users SET api_key = ?, provider = ? WHERE uid = ?').run(apiKey, provider, uid);
console.log(`SQLite: API key saved for user ${uid} with provider ${provider}.`);
resolve({ changes: this.changes });
return { changes: result.changes };
} catch (err) {
console.error('SQLite: Failed to save API key:', err);
throw err;
}
}
);
});
}
function update({ uid, displayName }) {
const db = sqliteClient.getDb();
return new Promise((resolve, reject) => {
db.run('UPDATE users SET display_name = ? WHERE uid = ?', [displayName, uid], function(err) {
if (err) reject(err);
else resolve({ changes: this.changes });
});
});
const result = db.prepare('UPDATE users SET display_name = ? WHERE uid = ?').run(displayName, uid);
return { changes: result.changes };
}
function deleteById(uid) {
const db = sqliteClient.getDb();
return new Promise((resolve, reject) => {
const userSessions = db.prepare('SELECT id FROM sessions WHERE uid = ?').all(uid);
const sessionIds = userSessions.map(s => s.id);
db.serialize(() => {
db.run("BEGIN TRANSACTION;");
try {
const transaction = db.transaction(() => {
if (sessionIds.length > 0) {
const placeholders = sessionIds.map(() => '?').join(',');
db.prepare(`DELETE FROM transcripts WHERE session_id IN (${placeholders})`).run(...sessionIds);
@ -82,20 +60,14 @@ function deleteById(uid) {
}
db.prepare('DELETE FROM prompt_presets WHERE uid = ? AND is_default = 0').run(uid);
db.prepare('DELETE FROM users WHERE uid = ?').run(uid);
});
db.run("COMMIT;", (err) => {
if (err) {
db.run("ROLLBACK;");
return reject(err);
}
resolve({ success: true });
});
try {
transaction();
return { success: true };
} catch (err) {
db.run("ROLLBACK;");
reject(err);
throw err;
}
});
});
}
module.exports = {

View File

@ -123,7 +123,7 @@ class AuthService {
const userState = this.getCurrentUser();
console.log('[AuthService] Broadcasting user state change:', userState);
BrowserWindow.getAllWindows().forEach(win => {
if (win && !win.isDestroyed()) {
if (win && !win.isDestroyed() && win.webContents && !win.webContents.isDestroyed()) {
win.webContents.send('user-state-changed', userState);
}
});

View File

@ -1,4 +1,4 @@
const sqlite3 = require('sqlite3').verbose();
const Database = require('better-sqlite3');
const path = require('path');
const LATEST_SCHEMA = require('../config/schema');
@ -10,28 +10,20 @@ class SQLiteClient {
}
connect(dbPath) {
return new Promise((resolve, reject) => {
if (this.db) {
console.log('[SQLiteClient] Already connected.');
return resolve();
return;
}
try {
this.dbPath = dbPath;
this.db = new sqlite3.Database(this.dbPath, (err) => {
if (err) {
console.error('[SQLiteClient] Could not connect to database', err);
return reject(err);
}
this.db = new Database(this.dbPath);
this.db.pragma('journal_mode = WAL');
console.log('[SQLiteClient] Connected successfully to:', this.dbPath);
this.db.run('PRAGMA journal_mode = WAL;', (err) => {
if (err) {
return reject(err);
} catch (err) {
console.error('[SQLiteClient] Could not connect to database', err);
throw err;
}
resolve();
});
});
});
}
getDb() {
@ -41,51 +33,39 @@ class SQLiteClient {
return this.db;
}
async synchronizeSchema() {
synchronizeSchema() {
console.log('[DB Sync] Starting schema synchronization...');
const tablesInDb = await this.getTablesFromDb();
const tablesInDb = this.getTablesFromDb();
for (const tableName of Object.keys(LATEST_SCHEMA)) {
const tableSchema = LATEST_SCHEMA[tableName];
if (!tablesInDb.includes(tableName)) {
// Table doesn't exist, create it
await this.createTable(tableName, tableSchema);
this.createTable(tableName, tableSchema);
} else {
// Table exists, check for missing columns
await this.updateTable(tableName, tableSchema);
this.updateTable(tableName, tableSchema);
}
}
console.log('[DB Sync] Schema synchronization finished.');
}
async getTablesFromDb() {
return new Promise((resolve, reject) => {
this.db.all("SELECT name FROM sqlite_master WHERE type='table'", (err, tables) => {
if (err) return reject(err);
resolve(tables.map(t => t.name));
});
});
getTablesFromDb() {
const tables = this.db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all();
return tables.map(t => t.name);
}
async createTable(tableName, tableSchema) {
return new Promise((resolve, reject) => {
createTable(tableName, tableSchema) {
const columnDefs = tableSchema.columns.map(col => `"${col.name}" ${col.type}`).join(', ');
const query = `CREATE TABLE IF NOT EXISTS "${tableName}" (${columnDefs})`;
console.log(`[DB Sync] Creating table: ${tableName}`);
this.db.run(query, (err) => {
if (err) return reject(err);
resolve();
});
});
this.db.prepare(query).run();
}
async updateTable(tableName, tableSchema) {
return new Promise((resolve, reject) => {
this.db.all(`PRAGMA table_info("${tableName}")`, async (err, existingColumns) => {
if (err) return reject(err);
updateTable(tableName, tableSchema) {
const existingColumns = this.db.prepare(`PRAGMA table_info("${tableName}")`).all();
const existingColumnNames = existingColumns.map(c => c.name);
const columnsToAdd = tableSchema.columns.filter(col => !existingColumnNames.includes(col.name));
@ -93,28 +73,16 @@ class SQLiteClient {
console.log(`[DB Sync] Updating table: ${tableName}. Adding columns: ${columnsToAdd.map(c=>c.name).join(', ')}`);
for (const column of columnsToAdd) {
const addColumnQuery = `ALTER TABLE "${tableName}" ADD COLUMN "${column.name}" ${column.type}`;
try {
await this.runQuery(addColumnQuery);
} catch (alterErr) {
return reject(alterErr);
this.db.prepare(addColumnQuery).run();
}
}
}
resolve();
});
});
}
async runQuery(query, params = []) {
return new Promise((resolve, reject) => {
this.db.run(query, params, function(err) {
if (err) return reject(err);
resolve(this);
});
});
runQuery(query, params = []) {
return this.db.prepare(query).run(params);
}
async cleanupEmptySessions() {
cleanupEmptySessions() {
console.log('[DB Cleanup] Checking for empty sessions...');
const query = `
SELECT s.id FROM sessions s
@ -124,16 +92,11 @@ class SQLiteClient {
WHERE t.id IS NULL AND a.id IS NULL AND su.session_id IS NULL
`;
return new Promise((resolve, reject) => {
this.db.all(query, [], (err, rows) => {
if (err) {
console.error('[DB Cleanup] Error finding empty sessions:', err);
return reject(err);
}
const rows = this.db.prepare(query).all();
if (rows.length === 0) {
console.log('[DB Cleanup] No empty sessions found.');
return resolve();
return;
}
const idsToDelete = rows.map(r => r.id);
@ -141,36 +104,23 @@ class SQLiteClient {
const deleteQuery = `DELETE FROM sessions WHERE id IN (${placeholders})`;
console.log(`[DB Cleanup] Found ${idsToDelete.length} empty sessions. Deleting...`);
this.db.run(deleteQuery, idsToDelete, function(deleteErr) {
if (deleteErr) {
console.error('[DB Cleanup] Error deleting empty sessions:', deleteErr);
return reject(deleteErr);
}
console.log(`[DB Cleanup] Successfully deleted ${this.changes} empty sessions.`);
resolve();
});
});
});
const result = this.db.prepare(deleteQuery).run(idsToDelete);
console.log(`[DB Cleanup] Successfully deleted ${result.changes} empty sessions.`);
}
async initTables() {
await this.synchronizeSchema();
await this.initDefaultData();
initTables() {
this.synchronizeSchema();
this.initDefaultData();
}
async initDefaultData() {
return new Promise((resolve, reject) => {
initDefaultData() {
const now = Math.floor(Date.now() / 1000);
const initUserQuery = `
INSERT OR IGNORE INTO users (uid, display_name, email, created_at)
VALUES (?, ?, ?, ?)
`;
this.db.run(initUserQuery, [this.defaultUserId, 'Default User', 'contact@pickle.com', now], (err) => {
if (err) {
console.error('Failed to initialize default user:', err);
return reject(err);
}
this.db.prepare(initUserQuery).run(this.defaultUserId, 'Default User', 'contact@pickle.com', now);
const defaultPresets = [
['school', 'School', 'You are a school and lecture assistant. Your goal is to help the user, a student, understand academic material and answer questions.\n\nWhenever a question appears on the user\'s screen or is asked aloud, you provide a direct, step-by-step answer, showing all necessary reasoning or calculations.\n\nIf the user is watching a lecture or working through new material, you offer concise explanations of key concepts and clarify definitions as they come up.', 1],
@ -189,27 +139,18 @@ class SQLiteClient {
stmt.run(preset[0], this.defaultUserId, preset[1], preset[2], preset[3], now);
}
stmt.finalize((err) => {
if (err) {
console.error('Failed to finalize preset statement:', err);
return reject(err);
}
console.log('Default data initialized.');
resolve();
});
});
});
}
async markPermissionsAsCompleted() {
markPermissionsAsCompleted() {
return this.query(
'INSERT OR REPLACE INTO system_settings (key, value) VALUES (?, ?)',
['permissions_completed', 'true']
);
}
async checkPermissionsCompleted() {
const result = await this.query(
checkPermissionsCompleted() {
const result = this.query(
'SELECT value FROM system_settings WHERE key = ?',
['permissions_completed']
);
@ -218,43 +159,32 @@ class SQLiteClient {
close() {
if (this.db) {
this.db.close((err) => {
if (err) {
console.error('SQLite connection close failed:', err);
} else {
try {
this.db.close();
console.log('SQLite connection closed.');
} catch (err) {
console.error('SQLite connection close failed:', err);
}
});
this.db = null;
}
}
async query(sql, params = []) {
return new Promise((resolve, reject) => {
query(sql, params = []) {
if (!this.db) {
return reject(new Error('Database not connected'));
throw new Error('Database not connected');
}
try {
if (sql.toUpperCase().startsWith('SELECT')) {
this.db.all(sql, params, (err, rows) => {
if (err) {
return this.db.prepare(sql).all(params);
} else {
const result = this.db.prepare(sql).run(params);
return { changes: result.changes, lastID: result.lastID };
}
} catch (err) {
console.error('Query error:', err);
reject(err);
} else {
resolve(rows);
throw err;
}
});
} else {
this.db.run(sql, params, function(err) {
if (err) {
console.error('Query error:', err);
reject(err);
} else {
resolve({ changes: this.changes, lastID: this.lastID });
}
});
}
});
}
}

View File

@ -1,7 +1,6 @@
const { BrowserWindow, globalShortcut, ipcMain, screen, app, shell, desktopCapturer } = require('electron');
const WindowLayoutManager = require('./windowLayoutManager');
const SmoothMovementManager = require('./smoothMovementManager');
const liquidGlass = require('electron-liquid-glass');
const path = require('node:path');
const fs = require('node:fs');
const os = require('os');
@ -15,6 +14,7 @@ const fetch = require('node-fetch');
/* ────────────────[ GLASS BYPASS ]─────────────── */
let liquidGlass;
const isLiquidGlassSupported = () => {
if (process.platform !== 'darwin') {
return false;
@ -23,7 +23,15 @@ const isLiquidGlassSupported = () => {
// return majorVersion >= 25; // macOS 26+ (Darwin 25+)
return majorVersion >= 26; // See you soon!
};
const shouldUseLiquidGlass = isLiquidGlassSupported();
let shouldUseLiquidGlass = isLiquidGlassSupported();
if (shouldUseLiquidGlass) {
try {
liquidGlass = require('electron-liquid-glass');
} catch (e) {
console.warn('Could not load optional dependency "electron-liquid-glass". The feature will be disabled.');
shouldUseLiquidGlass = false;
}
}
/* ────────────────[ GLASS BYPASS ]─────────────── */
let isContentProtectionOn = true;

View File

@ -2,31 +2,23 @@ const sqliteClient = require('../../../common/services/sqliteClient');
function addAiMessage({ sessionId, role, content, model = 'gpt-4.1' }) {
const db = sqliteClient.getDb();
return new Promise((resolve, reject) => {
const messageId = require('crypto').randomUUID();
const now = Math.floor(Date.now() / 1000);
const query = `INSERT INTO ai_messages (id, session_id, sent_at, role, content, model, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)`;
db.run(query, [messageId, sessionId, now, role, content, model, now], function(err) {
if (err) {
try {
db.prepare(query).run(messageId, sessionId, now, role, content, model, now);
return { id: messageId };
} catch (err) {
console.error('SQLite: Failed to add AI message:', err);
reject(err);
throw err;
}
else {
resolve({ id: messageId });
}
});
});
}
function getAllAiMessagesBySessionId(sessionId) {
const db = sqliteClient.getDb();
return new Promise((resolve, reject) => {
const query = "SELECT * FROM ai_messages WHERE session_id = ? ORDER BY sent_at ASC";
db.all(query, [sessionId], (err, rows) => {
if (err) reject(err);
else resolve(rows);
});
});
return db.prepare(query).all(sessionId);
}
module.exports = {

View File

@ -2,33 +2,23 @@ const sqliteClient = require('../../../../common/services/sqliteClient');
function addTranscript({ sessionId, speaker, text }) {
const db = sqliteClient.getDb();
return new Promise((resolve, reject) => {
const transcriptId = require('crypto').randomUUID();
const now = Math.floor(Date.now() / 1000);
const query = `INSERT INTO transcripts (id, session_id, start_at, speaker, text, created_at) VALUES (?, ?, ?, ?, ?, ?)`;
db.run(query, [transcriptId, sessionId, now, speaker, text, now], function(err) {
if (err) {
try {
db.prepare(query).run(transcriptId, sessionId, now, speaker, text, now);
return { id: transcriptId };
} catch (err) {
console.error('Error adding transcript:', err);
reject(err);
} else {
resolve({ id: transcriptId });
throw err;
}
});
});
}
function getAllTranscriptsBySessionId(sessionId) {
const db = sqliteClient.getDb();
return new Promise((resolve, reject) => {
const query = "SELECT * FROM transcripts WHERE session_id = ? ORDER BY start_at ASC";
db.all(query, [sessionId], (err, rows) => {
if (err) {
reject(err);
} else {
resolve(rows);
}
});
});
return db.prepare(query).all(sessionId);
}
module.exports = {

View File

@ -2,7 +2,6 @@ const sqliteClient = require('../../../../common/services/sqliteClient');
function saveSummary({ sessionId, tldr, text, bullet_json, action_json, model = 'gpt-4.1' }) {
const db = sqliteClient.getDb();
return new Promise((resolve, reject) => {
const now = Math.floor(Date.now() / 1000);
const query = `
INSERT INTO summaries (session_id, generated_at, model, text, tldr, bullet_json, action_json, updated_at)
@ -16,29 +15,20 @@ function saveSummary({ sessionId, tldr, text, bullet_json, action_json, model =
action_json=excluded.action_json,
updated_at=excluded.updated_at
`;
db.run(query, [sessionId, now, model, text, tldr, bullet_json, action_json, now], function(err) {
if (err) {
try {
const result = db.prepare(query).run(sessionId, now, model, text, tldr, bullet_json, action_json, now);
return { changes: result.changes };
} catch (err) {
console.error('Error saving summary:', err);
reject(err);
} else {
resolve({ changes: this.changes });
throw err;
}
});
});
}
function getSummaryBySessionId(sessionId) {
const db = sqliteClient.getDb();
return new Promise((resolve, reject) => {
const query = "SELECT * FROM summaries WHERE session_id = ?";
db.get(query, [sessionId], (err, row) => {
if (err) {
reject(err);
} else {
resolve(row || null);
}
});
});
return db.prepare(query).get(sessionId) || null;
}
module.exports = {

View File

@ -2,102 +2,92 @@ const sqliteClient = require('../../../common/services/sqliteClient');
function getPresets(uid) {
const db = sqliteClient.getDb();
return new Promise((resolve, reject) => {
const query = `
SELECT * FROM prompt_presets
WHERE uid = ? OR is_default = 1
ORDER BY is_default DESC, title ASC
`;
db.all(query, [uid], (err, rows) => {
if (err) {
try {
return db.prepare(query).all(uid) || [];
} catch (err) {
console.error('SQLite: Failed to get presets:', err);
reject(err);
} else {
resolve(rows || []);
throw err;
}
});
});
}
function getPresetTemplates() {
const db = sqliteClient.getDb();
return new Promise((resolve, reject) => {
const query = `
SELECT * FROM prompt_presets
WHERE is_default = 1
ORDER BY title ASC
`;
db.all(query, [], (err, rows) => {
if (err) {
try {
return db.prepare(query).all() || [];
} catch (err) {
console.error('SQLite: Failed to get preset templates:', err);
reject(err);
} else {
resolve(rows || []);
throw err;
}
});
});
}
function createPreset({ uid, title, prompt }) {
const db = sqliteClient.getDb();
return new Promise((resolve, reject) => {
const id = require('crypto').randomUUID();
const now = Math.floor(Date.now() / 1000);
const query = `
INSERT INTO prompt_presets (id, uid, title, prompt, is_default, created_at, sync_state)
VALUES (?, ?, ?, ?, 0, ?, 'dirty')
`;
db.run(query, [id, uid, title, prompt, now], function(err) {
if (err) {
try {
db.prepare(query).run(id, uid, title, prompt, now);
return { id };
} catch (err) {
console.error('SQLite: Failed to create preset:', err);
reject(err);
} else {
resolve({ id });
throw err;
}
});
});
}
function updatePreset(id, { title, prompt }, uid) {
const db = sqliteClient.getDb();
return new Promise((resolve, reject) => {
const now = Math.floor(Date.now() / 1000);
const query = `
UPDATE prompt_presets
SET title = ?, prompt = ?, sync_state = 'dirty', updated_at = ?
WHERE id = ? AND uid = ? AND is_default = 0
`;
db.run(query, [title, prompt, now, id, uid], function(err) {
if (err) {
console.error('SQLite: Failed to update preset:', err);
reject(err);
} else if (this.changes === 0) {
reject(new Error('Preset not found, is default, or permission denied'));
} else {
resolve({ changes: this.changes });
try {
const result = db.prepare(query).run(title, prompt, now, id, uid);
if (result.changes === 0) {
throw new Error('Preset not found, is default, or permission denied');
}
return { changes: result.changes };
} catch (err) {
console.error('SQLite: Failed to update preset:', err);
throw err;
}
});
});
}
function deletePreset(id, uid) {
const db = sqliteClient.getDb();
return new Promise((resolve, reject) => {
const query = `
DELETE FROM prompt_presets
WHERE id = ? AND uid = ? AND is_default = 0
`;
db.run(query, [id, uid], function(err) {
if (err) {
console.error('SQLite: Failed to delete preset:', err);
reject(err);
} else if (this.changes === 0) {
reject(new Error('Preset not found, is default, or permission denied'));
} else {
resolve({ changes: this.changes });
try {
const result = db.prepare(query).run(id, uid);
if (result.changes === 0) {
throw new Error('Preset not found, is default, or permission denied');
}
return { changes: result.changes };
} catch (err) {
console.error('SQLite: Failed to delete preset:', err);
throw err;
}
});
});
}
module.exports = {

View File

@ -18,7 +18,6 @@ const { initializeFirebase } = require('./common/services/firebaseClient');
const databaseInitializer = require('./common/services/databaseInitializer');
const authService = require('./common/services/authService');
const path = require('node:path');
const { Deeplink } = require('electron-deeplink');
const express = require('express');
const fetch = require('node-fetch');
const { autoUpdater } = require('electron-updater');
@ -33,72 +32,86 @@ let WEB_PORT = 3000;
const listenService = new ListenService();
// Make listenService globally accessible so other modules (e.g., windowManager, askService) can reuse the same instance
global.listenService = listenService;
let deeplink = null; // Initialize as null
let pendingDeepLinkUrl = null; // Store any deep link that arrives before initialization
function createMainWindows() {
createWindows();
// Native deep link handling - cross-platform compatible
let pendingDeepLinkUrl = null;
const { windowPool } = require('./electron/windowManager');
const headerWindow = windowPool.get('header');
function setupProtocolHandling() {
// Protocol registration - must be done before app is ready
if (!app.isDefaultProtocolClient('pickleglass')) {
app.setAsDefaultProtocolClient('pickleglass');
console.log('[Protocol] Set as default protocol client for pickleglass://');
}
// Initialize deeplink after windows are created
if (!deeplink && headerWindow) {
try {
deeplink = new Deeplink({
app,
mainWindow: headerWindow,
protocol: 'pickleglass',
isDev: !app.isPackaged,
debugLogging: true
// Handle protocol URLs on Windows/Linux
app.on('second-instance', (event, commandLine, workingDirectory) => {
// Focus existing window first
focusMainWindow();
// Look for protocol URL in command line arguments
const protocolUrl = commandLine.find(arg => arg.startsWith('pickleglass://'));
if (protocolUrl) {
console.log('[Protocol] Received URL from second instance:', protocolUrl);
handleCustomUrl(protocolUrl);
}
});
deeplink.on('received', (url) => {
console.log('[deeplink] received:', url);
// Handle protocol URLs on macOS
app.on('open-url', (event, url) => {
event.preventDefault();
console.log('[Protocol] Received URL via open-url:', url);
if (!url || !url.startsWith('pickleglass://')) {
console.warn('[Protocol] Invalid URL format:', url);
return;
}
if (app.isReady()) {
handleCustomUrl(url);
} else {
pendingDeepLinkUrl = url;
console.log('[Protocol] App not ready, storing URL for later');
}
});
console.log('[deeplink] Initialized with main window');
// Handle any pending deep link
if (pendingDeepLinkUrl) {
console.log('[deeplink] Processing pending deep link:', pendingDeepLinkUrl);
handleCustomUrl(pendingDeepLinkUrl);
pendingDeepLinkUrl = null;
}
} catch (error) {
console.error('[deeplink] Failed to initialize deep link:', error);
deeplink = null;
}
}
}
function focusMainWindow() {
const { windowPool } = require('./electron/windowManager');
if (windowPool) {
const header = windowPool.get('header');
if (header && !header.isDestroyed()) {
if (header.isMinimized()) header.restore();
header.focus();
return true;
}
}
// Fallback: focus any available window
const windows = BrowserWindow.getAllWindows();
if (windows.length > 0) {
const mainWindow = windows[0];
if (!mainWindow.isDestroyed()) {
if (mainWindow.isMinimized()) mainWindow.restore();
mainWindow.focus();
return true;
}
}
return false;
}
// Setup protocol handling before app.whenReady()
setupProtocolHandling();
app.whenReady().then(async () => {
// Single instance lock - must be first
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
app.quit();
return;
} else {
app.on('second-instance', (event, commandLine, workingDirectory) => {
const { windowPool } = require('./electron/windowManager');
if (windowPool) {
const header = windowPool.get('header');
if (header) {
if (header.isMinimized()) header.restore();
header.focus();
return;
}
}
const windows = BrowserWindow.getAllWindows();
if (windows.length > 0) {
const mainWindow = windows[0];
if (mainWindow.isMinimized()) mainWindow.restore();
mainWindow.focus();
}
});
}
// Initialize core services
initializeFirebase();
databaseInitializer.initialize()
@ -121,9 +134,16 @@ app.whenReady().then(async () => {
WEB_PORT = await startWebStack();
console.log('Web front-end listening on', WEB_PORT);
createMainWindows();
createWindows();
initAutoUpdater();
// Process any pending deep link after everything is initialized
if (pendingDeepLinkUrl) {
console.log('[Protocol] Processing pending URL:', pendingDeepLinkUrl);
handleCustomUrl(pendingDeepLinkUrl);
pendingDeepLinkUrl = null;
}
});
app.on('window-all-closed', () => {
@ -142,34 +162,17 @@ app.on('before-quit', async () => {
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createMainWindows();
createWindows();
}
});
// Add macOS native deep link handling as fallback
app.on('open-url', (event, url) => {
event.preventDefault();
console.log('[app] open-url received:', url);
if (!deeplink) {
// Store the URL if deeplink isn't ready yet
pendingDeepLinkUrl = url;
console.log('[app] Deep link stored for later processing');
} else {
handleCustomUrl(url);
}
});
// Ensure app can handle the protocol
app.setAsDefaultProtocolClient('pickleglass');
function setupGeneralIpcHandlers() {
const userRepository = require('./common/repositories/user');
const presetRepository = require('./common/repositories/preset');
ipcMain.handle('save-api-key', async (event, apiKey) => {
ipcMain.handle('save-api-key', (event, apiKey) => {
try {
await userRepository.saveApiKey(apiKey, authService.getCurrentUserId());
userRepository.saveApiKey(apiKey, authService.getCurrentUserId());
BrowserWindow.getAllWindows().forEach(win => {
win.webContents.send('api-key-updated');
});
@ -180,12 +183,12 @@ function setupGeneralIpcHandlers() {
}
});
ipcMain.handle('get-user-presets', async () => {
return await presetRepository.getPresets(authService.getCurrentUserId());
ipcMain.handle('get-user-presets', () => {
return presetRepository.getPresets(authService.getCurrentUserId());
});
ipcMain.handle('get-preset-templates', async () => {
return await presetRepository.getPresetTemplates();
ipcMain.handle('get-preset-templates', () => {
return presetRepository.getPresetTemplates();
});
ipcMain.handle('start-firebase-auth', async () => {
@ -204,7 +207,7 @@ function setupGeneralIpcHandlers() {
return process.env.pickleglass_WEB_URL || 'http://localhost:3000';
});
ipcMain.handle('get-current-user', async () => {
ipcMain.handle('get-current-user', () => {
return authService.getCurrentUser();
});
@ -220,72 +223,72 @@ function setupWebDataHandlers() {
const userRepository = require('./common/repositories/user');
const presetRepository = require('./common/repositories/preset');
const handleRequest = async (channel, responseChannel, payload) => {
const handleRequest = (channel, responseChannel, payload) => {
let result;
const currentUserId = authService.getCurrentUserId();
try {
switch (channel) {
// SESSION
case 'get-sessions':
result = await sessionRepository.getAllByUserId(currentUserId);
result = sessionRepository.getAllByUserId(currentUserId);
break;
case 'get-session-details':
const session = await sessionRepository.getById(payload);
const session = sessionRepository.getById(payload);
if (!session) {
result = null;
break;
}
const transcripts = await sttRepository.getAllTranscriptsBySessionId(payload);
const ai_messages = await askRepository.getAllAiMessagesBySessionId(payload);
const summary = await summaryRepository.getSummaryBySessionId(payload);
const transcripts = sttRepository.getAllTranscriptsBySessionId(payload);
const ai_messages = askRepository.getAllAiMessagesBySessionId(payload);
const summary = summaryRepository.getSummaryBySessionId(payload);
result = { session, transcripts, ai_messages, summary };
break;
case 'delete-session':
result = await sessionRepository.deleteWithRelatedData(payload);
result = sessionRepository.deleteWithRelatedData(payload);
break;
case 'create-session':
const id = await sessionRepository.create(currentUserId, 'ask');
const id = sessionRepository.create(currentUserId, 'ask');
if (payload.title) {
await sessionRepository.updateTitle(id, payload.title);
sessionRepository.updateTitle(id, payload.title);
}
result = { id };
break;
// USER
case 'get-user-profile':
result = await userRepository.getById(currentUserId);
result = userRepository.getById(currentUserId);
break;
case 'update-user-profile':
result = await userRepository.update({ uid: currentUserId, ...payload });
result = userRepository.update({ uid: currentUserId, ...payload });
break;
case 'find-or-create-user':
result = await userRepository.findOrCreate(payload);
result = userRepository.findOrCreate(payload);
break;
case 'save-api-key':
result = await userRepository.saveApiKey(payload, currentUserId);
result = userRepository.saveApiKey(payload, currentUserId);
break;
case 'check-api-key-status':
const user = await userRepository.getById(currentUserId);
const user = userRepository.getById(currentUserId);
result = { hasApiKey: !!user?.api_key && user.api_key.length > 0 };
break;
case 'delete-account':
result = await userRepository.deleteById(currentUserId);
result = userRepository.deleteById(currentUserId);
break;
// PRESET
case 'get-presets':
result = await presetRepository.getPresets(currentUserId);
result = presetRepository.getPresets(currentUserId);
break;
case 'create-preset':
result = await presetRepository.create({ ...payload, uid: currentUserId });
result = presetRepository.create({ ...payload, uid: currentUserId });
settingsService.notifyPresetUpdate('created', result.id, payload.title);
break;
case 'update-preset':
result = await presetRepository.update(payload.id, payload.data, currentUserId);
result = presetRepository.update(payload.id, payload.data, currentUserId);
settingsService.notifyPresetUpdate('updated', payload.id, payload.data.title);
break;
case 'delete-preset':
result = await presetRepository.delete(payload, currentUserId);
result = presetRepository.delete(payload, currentUserId);
settingsService.notifyPresetUpdate('deleted', payload);
break;
@ -295,13 +298,13 @@ function setupWebDataHandlers() {
const batchResult = {};
if (includes.includes('profile')) {
batchResult.profile = await userRepository.getById(currentUserId);
batchResult.profile = userRepository.getById(currentUserId);
}
if (includes.includes('presets')) {
batchResult.presets = await presetRepository.getPresets(currentUserId);
batchResult.presets = presetRepository.getPresets(currentUserId);
}
if (includes.includes('sessions')) {
batchResult.sessions = await sessionRepository.getAllByUserId(currentUserId);
batchResult.sessions = sessionRepository.getAllByUserId(currentUserId);
}
result = batchResult;
break;
@ -392,7 +395,7 @@ async function handleFirebaseAuthCallback(params) {
};
// 1. Sync user data to local DB
await userRepository.findOrCreate(firebaseUser);
userRepository.findOrCreate(firebaseUser);
console.log('[Auth] User data synced with local DB.');
// 2. Sign in using the authService in the main process