1 line
62 KiB
JSON
1 line
62 KiB
JSON
{"repo": "amoguse3/flow-app", "files": {"out/main/index.js": {"9afc3b31c06b42783c3a2594e9208c01e5127acd": {"t": 1785745554.991958, "n": 14715, "h": "b9da701690a2faa0", "c": "\"use strict\";\nconst electron = require(\"electron\");\nconst path = require(\"path\");\nconst memoryIpc = require(\"./chunks/memory-ipc-bLie0Z8U.js\");\nconst electronUpdater = require(\"electron-updater\");\nrequire(\"node:vm\");\nrequire(\"crypto\");\nrequire(\"fs\");\nrequire(\"sql.js\");\nconst is = {\n dev: !electron.app.isPackaged\n};\nconst platform = {\n isWindows: process.platform === \"win32\",\n isMacOS: process.platform === \"darwin\",\n isLinux: process.platform === \"linux\"\n};\nconst electronApp = {\n setAppUserModelId(id) {\n if (platform.isWindows)\n electron.app.setAppUserModelId(is.dev ? process.execPath : id);\n },\n setAutoLaunch(auto) {\n if (platform.isLinux)\n return false;\n const isOpenAtLogin = () => {\n return electron.app.getLoginItemSettings().openAtLogin;\n };\n if (isOpenAtLogin() !== auto) {\n electron.app.setLoginItemSettings({\n openAtLogin: auto,\n path: process.execPath\n });\n return isOpenAtLogin() === auto;\n } else {\n return true;\n }\n },\n skipProxy() {\n return electron.session.defaultSession.setProxy({ mode: \"direct\" });\n }\n};\nconst optimizer = {\n watchWindowShortcuts(window, shortcutOptions) {\n if (!window)\n return;\n const { webContents } = window;\n const { escToCloseWindow = false, zoom = false } = shortcutOptions || {};\n webContents.on(\"before-input-event\", (event, input) => {\n if (input.type === \"keyDown\") {\n if (!is.dev) {\n if (input.code === \"KeyR\" && (input.control || input.meta))\n event.preventDefault();\n } else {\n if (input.code === \"F12\") {\n if (webContents.isDevToolsOpened()) {\n webContents.closeDevTools();\n } else {\n webContents.openDevTools({ mode: \"undocked\" });\n console.log(\"Open dev tool...\");\n }\n }\n }\n if (escToCloseWindow) {\n if (input.code === \"Escape\" && input.key !== \"Process\") {\n window.close();\n event.preventDefault();\n }\n }\n if (!zoom) {\n if (input.code === \"Minus\" && (input.control || input.meta))\n event.preventDefault();\n if (input.code === \"Equal\" && input.shift && (input.control || input.meta))\n event.preventDefault();\n }\n }\n });\n },\n registerFramelessWindowIpc() {\n electron.ipcMain.on(\"win:invoke\", (event, action) => {\n const win = electron.BrowserWindow.fromWebContents(event.sender);\n if (win) {\n if (action === \"show\") {\n win.show();\n } else if (action === \"showInactive\") {\n win.showInactive();\n } else if (action === \"min\") {\n win.minimize();\n } else if (action === \"max\") {\n const isMaximized = win.isMaximized();\n if (isMaximized) {\n win.unmaximize();\n } else {\n win.maximize();\n }\n } else if (action === \"close\") {\n win.close();\n }\n }\n });\n }\n};\nfunction createMainWindow() {\n const win = new electron.BrowserWindow({\n width: 1280,\n height: 720,\n minWidth: 800,\n minHeight: 600,\n frame: false,\n transparent: false,\n backgroundColor: \"#080606\",\n titleBarStyle: \"hidden\",\n titleBarOverlay: {\n color: \"#080606\",\n symbolColor: \"#d97706\",\n height: 36\n },\n webPreferences: {\n preload: path.join(__dirname, \"../preload/index.js\"),\n sandbox: true,\n contextIsolation: true,\n nodeIntegration: false,\n webSecurity: true\n },\n show: false\n });\n win.on(\"ready-to-show\", () => {\n win.show();\n });\n win.webContents.on(\"will-navigate\", (event, url) => {\n if (!url.startsWith(\"file://\") && !url.startsWith(\"http://localhost\")) {\n event.preventDefault();\n }\n });\n win.webContents.setWindowOpenHandler(({ url }) => {\n if (url.startsWith(\"https://\") || url.startsWith(\"http://\")) {\n electron.shell.openExternal(url);\n }\n return { action: \"deny\" };\n });\n if (!is.dev) {\n win.webContents.on(\"before-input-event\", (event, input) => {\n if (input.key === \"F12\" || input.control && input.shift && (input.key === \"I\" || input.key === \"J\" || input.key === \"C\") || input.control && input.key === \"u\") {\n event.preventDefault();\n }\n });\n }\n if (is.dev && process.env[\"ELECTRON_RENDERER_URL\"]) {\n win.loadURL(process.env[\"ELECTRON_RENDERER_URL\"]);\n } else {\n win.loadFile(path.join(__dirname, \"../renderer/index.html\"));\n }\n return win;\n}\nlet tray = null;\nfunction createTray(getWindow2) {\n const iconPath = path.join(__dirname, \"../../resources/icon.png\");\n let icon;\n try {\n icon = electron.nativeImage.createFromPath(iconPath).resize({ width: 16, height: 16 });\n } catch {\n icon = electron.nativeImage.createEmpty();\n }\n tray = new electron.Tray(icon);\n tray.setToolTip(\"Wispucci Ai beta\");\n const contextMenu = electron.Menu.buildFromTemplate([\n {\n label: \"Show Wispucci AI beta\",\n click: () => {\n const win = getWindow2();\n if (win) {\n win.show();\n win.focus();\n }\n }\n },\n {\n label: \"Ascunde\",\n click: () => {\n const win = getWindow2();\n if (win) win.hide();\n }\n },\n { type: \"separator\" },\n {\n label: \"Ctrl+Shift+A — toggle rapid\",\n enabled: false\n },\n { type: \"separator\" },\n {\n label: \"Quit Wispucci AI beta\",\n click: () => {\n electron.app.quit();\n }\n }\n ]);\n tray.setContextMenu(contextMenu);\n tray.on(\"click\", () => {\n const win = getWindow2();\n if (!win) return;\n if (win.isVisible()) {\n win.focus();\n } else {\n win.show();\n win.focus();\n }\n });\n return tray;\n}\nfunction registerHotkey(getWindow2) {\n electron.globalShortcut.register(\"CommandOrControl+Shift+A\", () => {\n const win = getWindow2();\n if (!win) return;\n if (win.isVisible()) {\n win.hide();\n } else {\n win.show();\n win.focus();\n }\n });\n}\nfunction unregisterHotkeys() {\n electron.globalShortcut.unregisterAll();\n}\nlet overlayWindow = null;\nfunction shouldUseOverlay() {\n const profile = memoryIpc.getState(\"profile\");\n return Boolean(profile?.onboardingDone) && profile?.orbEnabled !== false;\n}\nconst REMINDERS = [\n \"Don't forget to drink water! 💧\",\n \"You have unfinished courses! 📚\",\n \"A short break helps your brain.\",\n \"Let's learn something new!\",\n \"Progress is built step by step.\",\n \"You're on the right track! ⭐\",\n \"Memory improves with daily reps.\",\n \"Focus. You can do this. 💪\",\n \"One small step today = success tomorrow.\",\n \"Did you check your tasks today?\"\n];\nfunction createWindow() {\n const { width: screenW, height: screenH } = electron.screen.getPrimaryDisplay().workAreaSize;\n const winW = 300;\n const winH = 350;\n const win = new electron.BrowserWindow({\n width: winW,\n height: winH,\n x: screenW - winW - 16,\n y: screenH - winH - 16,\n frame: false,\n transparent: true,\n alwaysOnTop: true,\n skipTaskbar: true,\n resizable: false,\n hasShadow: false,\n webPreferences: {\n preload: path.join(__dirname, \"../preload/overlay.js\"),\n sandbox: true,\n contextIsolation: true,\n nodeIntegration: false\n },\n show: false\n });\n if (is.dev && process.env[\"ELECTRON_RENDERER_URL\"]) {\n win.loadURL(process.env[\"ELECTRON_RENDERER_URL\"] + \"/overlay.html\");\n } else {\n win.loadFile(path.join(__dirname, \"../renderer/overlay.html\"));\n }\n win.setIgnoreMouseEvents(true, { forward: true });\n win.on(\"closed\", () => {\n overlayWindow = null;\n });\n return win;\n}\nfunction registerOverlayIpc(getMainWindow) {\n electron.ipcMain.on(\"overlay:setClickThrough\", (_, ignore) => {\n if (overlayWindow && !overlayWindow.isDestroyed()) {\n if (ignore) {\n overlayWindow.setIgnoreMouseEvents(true, { forward: true });\n } else {\n overlayWindow.setIgnoreMouseEvents(false);\n }\n }\n });\n electron.ipcMain.on(\"overlay:dragMove\", (_, dx, dy) => {\n if (overlayWindow && !overlayWindow.isDestroyed()) {\n const [x, y] = overlayWindow.getPosition();\n const [w, h] = overlayWindow.getSize();\n const display = electron.screen.getDisplayNearestPoint({ x, y });\n const { x: sx, y: sy, width: sw, height: sh } = display.workArea;\n const nx = Math.max(sx, Math.min(sx + sw - w, x + dx));\n const ny = Math.max(sy, Math.min(sy + sh - h, y + dy));\n overlayWindow.setPosition(nx, ny);\n }\n });\n electron.ipcMain.handle(\"overlay:getReminder\", () => {\n return { text: REMINDERS[Math.floor(Math.random() * REMINDERS.length)] };\n });\n electron.ipcMain.handle(\"overlay:getOrbSize\", () => {\n const profile = memoryIpc.getState(\"profile\");\n return profile?.orbSize || \"medium\";\n });\n electron.ipcMain.on(\"overlay:openMain\", () => {\n const main = getMainWindow();\n if (main) {\n main.show();\n main.focus();\n }\n overlayWindow?.hide();\n });\n electron.ipcMain.on(\"overlay:sendToChat\", (_, message) => {\n const main = getMainWindow();\n if (main) {\n main.show();\n main.focus();\n main.webContents.send(\"overlay:chatMessage\", message);\n }\n overlayWindow?.hide();\n });\n electron.ipcMain.handle(\"overlay:setEnabled\", (_, enabled) => {\n if (enabled) {\n ensureOverlayWindow();\n } else {\n hideOverlay();\n }\n });\n electron.ipcMain.handle(\"overlay:setSize\", (_, size) => {\n if (overlayWindow && !overlayWindow.isDestroyed()) {\n overlayWindow.webContents.send(\"overlay:sizeChange\", size);\n }\n });\n}\nfunction ensureOverlayWindow() {\n if (overlayWindow && !overlayWindow.isDestroyed()) return overlayWindow;\n overlayWindow = createWindow();\n return overlayWindow;\n}\nfunction initOverlay() {\n if (shouldUseOverlay()) {\n overlayWindow = createWindow();\n }\n}\nfunction showOverlay() {\n if (!shouldUseOverlay()) return;\n ensureOverlayWindow();\n if (overlayWindow && !overlayWindow.isDestroyed()) {\n overlayWindow.showInactive();\n }\n}\nfunction hideOverlay() {\n if (overlayWindow && !overlayWindow.isDestroyed()) {\n overlayWindow.hide();\n }\n}\nfunction sendUpdateStatus(mainWindow2, text) {\n if (!mainWindow2 || mainWindow2.isDestroyed()) return;\n mainWindow2.webContents.send(\"app:updateStatus\", text);\n}\nfunction initAutoUpdater(getMainWindow) {\n if (!electron.app.isPackaged) return;\n electronUpdater.autoUpdater.autoDownload = true;\n electronUpdater.autoUpdater.autoInstallOnAppQuit = true;\n electronUpdater.autoUpdater.on(\"checking-for-update\", () => {\n sendUpdateStatus(getMainWindow(), \"Checking for updates...\");\n });\n electronUpdater.autoUpdater.on(\"update-available\", (info) => {\n sendUpdateStatus(getMainWindow(), `Update available: v${info.version}`);\n });\n electronUpdater.autoUpdater.on(\"update-not-available\", () => {\n sendUpdateStatus(getMainWindow(), \"The app is up to date.\");\n });\n electronUpdater.autoUpdater.on(\"download-progress\", (progress) => {\n sendUpdateStatus(getMainWindow(), `Downloading update: ${Math.round(progress.percent)}%`);\n });\n electronUpdater.autoUpdater.on(\"update-downloaded\", (info) => {\n sendUpdateStatus(getMainWindow(), `Update downloaded (v${info.version}). It will install on the next close.`);\n });\n electronUpdater.autoUpdater.on(\"error\", (err) => {\n sendUpdateStatus(getMainWindow(), `Update error: ${err.message}`);\n });\n const safeCheck = () => {\n electronUpdater.autoUpdater.checkForUpdates().catch(() => {\n });\n };\n setTimeout(safeCheck, 15e3);\n setInterval(safeCheck, 30 * 60 * 1e3);\n}\nlet mainWindow = null;\nfunction getWindow() {\n return mainWindow;\n}\nfunction isDeepSeekKey(value) {\n return typeof value === \"string\" && /^sk-(?!ant-)/.test(value.trim());\n}\nfunction isGroqKey(value) {\n return typeof value === \"string\" && /^gsk_/.test(value.trim());\n}\nelectron.app.whenReady().then(async () => {\n electron.session.defaultSession.webRequest.onHeadersReceived((details, callback) => {\n callback({\n responseHeaders: {\n ...details.responseHeaders,\n \"Content-Security-Policy\": [\n \"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; connect-src 'self' https://api.groq.com https://api.deepseek.com https://wisp-flow.vercel.app; img-src 'self' data:; font-src 'self' https://fonts.gstatic.com\"\n ]\n }\n });\n });\n electron.app.on(\"web-contents-created\", (_event, contents) => {\n contents.on(\"will-attach-webview\", (event) => {\n event.preventDefault();\n });\n });\n electronApp.setAppUserModelId(\"app.wispflow.wispucci-ai-beta\");\n electron.app.on(\"browser-window-created\", (_, window) => {\n optimizer.watchWindowShortcuts(window);\n });\n await memoryIpc.initDB();\n const savedClaudeKey = memoryIpc.getState(\"claudeApiKey\");\n const envClaudeKey = process.env[\"DEEPSEEK_API_KEY\"] || process.env[\"CLAUDE_API_KEY\"] || process.env[\"ANTHROPIC_API_KEY\"] || \"\";\n const resolvedClaudeKey = isDeepSeekKey(savedClaudeKey) ? savedClaudeKey.trim() : isDeepSeekKey(envClaudeKey) ? envClaudeKey.trim() : \"\";\n memoryIpc.setClaudeApiKey(resolvedClaudeKey);\n if (resolvedClaudeKey && savedClaudeKey !== resolvedClaudeKey) {\n memoryIpc.setState(\"claudeApiKey\", resolvedClaudeKey);\n }\n const savedGroqKey = memoryIpc.getState(\"groqApiKey\");\n const envGroqKey = process.env[\"GROQ_API_KEY\"] || \"\";\n const resolvedGroqKey = isGroqKey(savedGroqKey) ? savedGroqKey.trim() : isGroqKey(envGroqKey) ? envGroqKey.trim() : \"\";\n memoryIpc.setGroqApiKey(resolvedGroqKey);\n if (resolvedGroqKey && savedGroqKey !== resolvedGroqKey) {\n memoryIpc.setState(\"groqApiKey\", resolvedGroqKey);\n }\n memoryIpc.registerIpcHandlers();\n memoryIpc.registerEducatorIpc();\n memoryIpc.reconcileInterruptedCourseGeneration();\n memoryIpc.registerVoiceIpc();\n memoryIpc.registerGamesIpc();\n memoryIpc.registerSyncIpc();\n memoryIpc.registerMemoryIpc();\n registerOverlayIpc(getWindow);\n mainWindow = createMainWindow();\n createTray(getWindow);\n registerHotkey(getWindow);\n initOverlay();\n memoryIpc.getMachineId();\n memoryIpc.startTelemetryLoop();\n initAutoUpdater(getWindow);\n mainWindow.on(\"minimize\", () => showOverlay());\n mainWindow.on(\"hide\", () => showOverlay());\n mainWindow.on(\"restore\", () => hideOverlay());\n mainWindow.on(\"show\", () => hideOverlay());\n mainWindow.on(\"closed\", () => {\n mainWindow = null;\n });\n electron.app.on(\"activate\", () => {\n if (electron.BrowserWindow.getAllWindows().length === 0) {\n mainWindow = createMainWindow();\n }\n });\n});\nelectron.app.on(\"will-quit\", () => {\n memoryIpc.saveDBSync();\n unregisterHotkeys();\n});\nelectron.app.on(\"window-all-closed\", () => {\n});\n"}, "5c6de16d5f00fb016ec6ac16b04e96c11dfc5211": {"t": 1785754165.331838, "n": 14715, "h": "b9da701690a2faa0", "c": "\"use strict\";\nconst electron = require(\"electron\");\nconst path = require(\"path\");\nconst memoryIpc = require(\"./chunks/memory-ipc-bLie0Z8U.js\");\nconst electronUpdater = require(\"electron-updater\");\nrequire(\"node:vm\");\nrequire(\"crypto\");\nrequire(\"fs\");\nrequire(\"sql.js\");\nconst is = {\n dev: !electron.app.isPackaged\n};\nconst platform = {\n isWindows: process.platform === \"win32\",\n isMacOS: process.platform === \"darwin\",\n isLinux: process.platform === \"linux\"\n};\nconst electronApp = {\n setAppUserModelId(id) {\n if (platform.isWindows)\n electron.app.setAppUserModelId(is.dev ? process.execPath : id);\n },\n setAutoLaunch(auto) {\n if (platform.isLinux)\n return false;\n const isOpenAtLogin = () => {\n return electron.app.getLoginItemSettings().openAtLogin;\n };\n if (isOpenAtLogin() !== auto) {\n electron.app.setLoginItemSettings({\n openAtLogin: auto,\n path: process.execPath\n });\n return isOpenAtLogin() === auto;\n } else {\n return true;\n }\n },\n skipProxy() {\n return electron.session.defaultSession.setProxy({ mode: \"direct\" });\n }\n};\nconst optimizer = {\n watchWindowShortcuts(window, shortcutOptions) {\n if (!window)\n return;\n const { webContents } = window;\n const { escToCloseWindow = false, zoom = false } = shortcutOptions || {};\n webContents.on(\"before-input-event\", (event, input) => {\n if (input.type === \"keyDown\") {\n if (!is.dev) {\n if (input.code === \"KeyR\" && (input.control || input.meta))\n event.preventDefault();\n } else {\n if (input.code === \"F12\") {\n if (webContents.isDevToolsOpened()) {\n webContents.closeDevTools();\n } else {\n webContents.openDevTools({ mode: \"undocked\" });\n console.log(\"Open dev tool...\");\n }\n }\n }\n if (escToCloseWindow) {\n if (input.code === \"Escape\" && input.key !== \"Process\") {\n window.close();\n event.preventDefault();\n }\n }\n if (!zoom) {\n if (input.code === \"Minus\" && (input.control || input.meta))\n event.preventDefault();\n if (input.code === \"Equal\" && input.shift && (input.control || input.meta))\n event.preventDefault();\n }\n }\n });\n },\n registerFramelessWindowIpc() {\n electron.ipcMain.on(\"win:invoke\", (event, action) => {\n const win = electron.BrowserWindow.fromWebContents(event.sender);\n if (win) {\n if (action === \"show\") {\n win.show();\n } else if (action === \"showInactive\") {\n win.showInactive();\n } else if (action === \"min\") {\n win.minimize();\n } else if (action === \"max\") {\n const isMaximized = win.isMaximized();\n if (isMaximized) {\n win.unmaximize();\n } else {\n win.maximize();\n }\n } else if (action === \"close\") {\n win.close();\n }\n }\n });\n }\n};\nfunction createMainWindow() {\n const win = new electron.BrowserWindow({\n width: 1280,\n height: 720,\n minWidth: 800,\n minHeight: 600,\n frame: false,\n transparent: false,\n backgroundColor: \"#080606\",\n titleBarStyle: \"hidden\",\n titleBarOverlay: {\n color: \"#080606\",\n symbolColor: \"#d97706\",\n height: 36\n },\n webPreferences: {\n preload: path.join(__dirname, \"../preload/index.js\"),\n sandbox: true,\n contextIsolation: true,\n nodeIntegration: false,\n webSecurity: true\n },\n show: false\n });\n win.on(\"ready-to-show\", () => {\n win.show();\n });\n win.webContents.on(\"will-navigate\", (event, url) => {\n if (!url.startsWith(\"file://\") && !url.startsWith(\"http://localhost\")) {\n event.preventDefault();\n }\n });\n win.webContents.setWindowOpenHandler(({ url }) => {\n if (url.startsWith(\"https://\") || url.startsWith(\"http://\")) {\n electron.shell.openExternal(url);\n }\n return { action: \"deny\" };\n });\n if (!is.dev) {\n win.webContents.on(\"before-input-event\", (event, input) => {\n if (input.key === \"F12\" || input.control && input.shift && (input.key === \"I\" || input.key === \"J\" || input.key === \"C\") || input.control && input.key === \"u\") {\n event.preventDefault();\n }\n });\n }\n if (is.dev && process.env[\"ELECTRON_RENDERER_URL\"]) {\n win.loadURL(process.env[\"ELECTRON_RENDERER_URL\"]);\n } else {\n win.loadFile(path.join(__dirname, \"../renderer/index.html\"));\n }\n return win;\n}\nlet tray = null;\nfunction createTray(getWindow2) {\n const iconPath = path.join(__dirname, \"../../resources/icon.png\");\n let icon;\n try {\n icon = electron.nativeImage.createFromPath(iconPath).resize({ width: 16, height: 16 });\n } catch {\n icon = electron.nativeImage.createEmpty();\n }\n tray = new electron.Tray(icon);\n tray.setToolTip(\"Wispucci Ai beta\");\n const contextMenu = electron.Menu.buildFromTemplate([\n {\n label: \"Show Wispucci AI beta\",\n click: () => {\n const win = getWindow2();\n if (win) {\n win.show();\n win.focus();\n }\n }\n },\n {\n label: \"Ascunde\",\n click: () => {\n const win = getWindow2();\n if (win) win.hide();\n }\n },\n { type: \"separator\" },\n {\n label: \"Ctrl+Shift+A — toggle rapid\",\n enabled: false\n },\n { type: \"separator\" },\n {\n label: \"Quit Wispucci AI beta\",\n click: () => {\n electron.app.quit();\n }\n }\n ]);\n tray.setContextMenu(contextMenu);\n tray.on(\"click\", () => {\n const win = getWindow2();\n if (!win) return;\n if (win.isVisible()) {\n win.focus();\n } else {\n win.show();\n win.focus();\n }\n });\n return tray;\n}\nfunction registerHotkey(getWindow2) {\n electron.globalShortcut.register(\"CommandOrControl+Shift+A\", () => {\n const win = getWindow2();\n if (!win) return;\n if (win.isVisible()) {\n win.hide();\n } else {\n win.show();\n win.focus();\n }\n });\n}\nfunction unregisterHotkeys() {\n electron.globalShortcut.unregisterAll();\n}\nlet overlayWindow = null;\nfunction shouldUseOverlay() {\n const profile = memoryIpc.getState(\"profile\");\n return Boolean(profile?.onboardingDone) && profile?.orbEnabled !== false;\n}\nconst REMINDERS = [\n \"Don't forget to drink water! 💧\",\n \"You have unfinished courses! 📚\",\n \"A short break helps your brain.\",\n \"Let's learn something new!\",\n \"Progress is built step by step.\",\n \"You're on the right track! ⭐\",\n \"Memory improves with daily reps.\",\n \"Focus. You can do this. 💪\",\n \"One small step today = success tomorrow.\",\n \"Did you check your tasks today?\"\n];\nfunction createWindow() {\n const { width: screenW, height: screenH } = electron.screen.getPrimaryDisplay().workAreaSize;\n const winW = 300;\n const winH = 350;\n const win = new electron.BrowserWindow({\n width: winW,\n height: winH,\n x: screenW - winW - 16,\n y: screenH - winH - 16,\n frame: false,\n transparent: true,\n alwaysOnTop: true,\n skipTaskbar: true,\n resizable: false,\n hasShadow: false,\n webPreferences: {\n preload: path.join(__dirname, \"../preload/overlay.js\"),\n sandbox: true,\n contextIsolation: true,\n nodeIntegration: false\n },\n show: false\n });\n if (is.dev && process.env[\"ELECTRON_RENDERER_URL\"]) {\n win.loadURL(process.env[\"ELECTRON_RENDERER_URL\"] + \"/overlay.html\");\n } else {\n win.loadFile(path.join(__dirname, \"../renderer/overlay.html\"));\n }\n win.setIgnoreMouseEvents(true, { forward: true });\n win.on(\"closed\", () => {\n overlayWindow = null;\n });\n return win;\n}\nfunction registerOverlayIpc(getMainWindow) {\n electron.ipcMain.on(\"overlay:setClickThrough\", (_, ignore) => {\n if (overlayWindow && !overlayWindow.isDestroyed()) {\n if (ignore) {\n overlayWindow.setIgnoreMouseEvents(true, { forward: true });\n } else {\n overlayWindow.setIgnoreMouseEvents(false);\n }\n }\n });\n electron.ipcMain.on(\"overlay:dragMove\", (_, dx, dy) => {\n if (overlayWindow && !overlayWindow.isDestroyed()) {\n const [x, y] = overlayWindow.getPosition();\n const [w, h] = overlayWindow.getSize();\n const display = electron.screen.getDisplayNearestPoint({ x, y });\n const { x: sx, y: sy, width: sw, height: sh } = display.workArea;\n const nx = Math.max(sx, Math.min(sx + sw - w, x + dx));\n const ny = Math.max(sy, Math.min(sy + sh - h, y + dy));\n overlayWindow.setPosition(nx, ny);\n }\n });\n electron.ipcMain.handle(\"overlay:getReminder\", () => {\n return { text: REMINDERS[Math.floor(Math.random() * REMINDERS.length)] };\n });\n electron.ipcMain.handle(\"overlay:getOrbSize\", () => {\n const profile = memoryIpc.getState(\"profile\");\n return profile?.orbSize || \"medium\";\n });\n electron.ipcMain.on(\"overlay:openMain\", () => {\n const main = getMainWindow();\n if (main) {\n main.show();\n main.focus();\n }\n overlayWindow?.hide();\n });\n electron.ipcMain.on(\"overlay:sendToChat\", (_, message) => {\n const main = getMainWindow();\n if (main) {\n main.show();\n main.focus();\n main.webContents.send(\"overlay:chatMessage\", message);\n }\n overlayWindow?.hide();\n });\n electron.ipcMain.handle(\"overlay:setEnabled\", (_, enabled) => {\n if (enabled) {\n ensureOverlayWindow();\n } else {\n hideOverlay();\n }\n });\n electron.ipcMain.handle(\"overlay:setSize\", (_, size) => {\n if (overlayWindow && !overlayWindow.isDestroyed()) {\n overlayWindow.webContents.send(\"overlay:sizeChange\", size);\n }\n });\n}\nfunction ensureOverlayWindow() {\n if (overlayWindow && !overlayWindow.isDestroyed()) return overlayWindow;\n overlayWindow = createWindow();\n return overlayWindow;\n}\nfunction initOverlay() {\n if (shouldUseOverlay()) {\n overlayWindow = createWindow();\n }\n}\nfunction showOverlay() {\n if (!shouldUseOverlay()) return;\n ensureOverlayWindow();\n if (overlayWindow && !overlayWindow.isDestroyed()) {\n overlayWindow.showInactive();\n }\n}\nfunction hideOverlay() {\n if (overlayWindow && !overlayWindow.isDestroyed()) {\n overlayWindow.hide();\n }\n}\nfunction sendUpdateStatus(mainWindow2, text) {\n if (!mainWindow2 || mainWindow2.isDestroyed()) return;\n mainWindow2.webContents.send(\"app:updateStatus\", text);\n}\nfunction initAutoUpdater(getMainWindow) {\n if (!electron.app.isPackaged) return;\n electronUpdater.autoUpdater.autoDownload = true;\n electronUpdater.autoUpdater.autoInstallOnAppQuit = true;\n electronUpdater.autoUpdater.on(\"checking-for-update\", () => {\n sendUpdateStatus(getMainWindow(), \"Checking for updates...\");\n });\n electronUpdater.autoUpdater.on(\"update-available\", (info) => {\n sendUpdateStatus(getMainWindow(), `Update available: v${info.version}`);\n });\n electronUpdater.autoUpdater.on(\"update-not-available\", () => {\n sendUpdateStatus(getMainWindow(), \"The app is up to date.\");\n });\n electronUpdater.autoUpdater.on(\"download-progress\", (progress) => {\n sendUpdateStatus(getMainWindow(), `Downloading update: ${Math.round(progress.percent)}%`);\n });\n electronUpdater.autoUpdater.on(\"update-downloaded\", (info) => {\n sendUpdateStatus(getMainWindow(), `Update downloaded (v${info.version}). It will install on the next close.`);\n });\n electronUpdater.autoUpdater.on(\"error\", (err) => {\n sendUpdateStatus(getMainWindow(), `Update error: ${err.message}`);\n });\n const safeCheck = () => {\n electronUpdater.autoUpdater.checkForUpdates().catch(() => {\n });\n };\n setTimeout(safeCheck, 15e3);\n setInterval(safeCheck, 30 * 60 * 1e3);\n}\nlet mainWindow = null;\nfunction getWindow() {\n return mainWindow;\n}\nfunction isDeepSeekKey(value) {\n return typeof value === \"string\" && /^sk-(?!ant-)/.test(value.trim());\n}\nfunction isGroqKey(value) {\n return typeof value === \"string\" && /^gsk_/.test(value.trim());\n}\nelectron.app.whenReady().then(async () => {\n electron.session.defaultSession.webRequest.onHeadersReceived((details, callback) => {\n callback({\n responseHeaders: {\n ...details.responseHeaders,\n \"Content-Security-Policy\": [\n \"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; connect-src 'self' https://api.groq.com https://api.deepseek.com https://wisp-flow.vercel.app; img-src 'self' data:; font-src 'self' https://fonts.gstatic.com\"\n ]\n }\n });\n });\n electron.app.on(\"web-contents-created\", (_event, contents) => {\n contents.on(\"will-attach-webview\", (event) => {\n event.preventDefault();\n });\n });\n electronApp.setAppUserModelId(\"app.wispflow.wispucci-ai-beta\");\n electron.app.on(\"browser-window-created\", (_, window) => {\n optimizer.watchWindowShortcuts(window);\n });\n await memoryIpc.initDB();\n const savedClaudeKey = memoryIpc.getState(\"claudeApiKey\");\n const envClaudeKey = process.env[\"DEEPSEEK_API_KEY\"] || process.env[\"CLAUDE_API_KEY\"] || process.env[\"ANTHROPIC_API_KEY\"] || \"\";\n const resolvedClaudeKey = isDeepSeekKey(savedClaudeKey) ? savedClaudeKey.trim() : isDeepSeekKey(envClaudeKey) ? envClaudeKey.trim() : \"\";\n memoryIpc.setClaudeApiKey(resolvedClaudeKey);\n if (resolvedClaudeKey && savedClaudeKey !== resolvedClaudeKey) {\n memoryIpc.setState(\"claudeApiKey\", resolvedClaudeKey);\n }\n const savedGroqKey = memoryIpc.getState(\"groqApiKey\");\n const envGroqKey = process.env[\"GROQ_API_KEY\"] || \"\";\n const resolvedGroqKey = isGroqKey(savedGroqKey) ? savedGroqKey.trim() : isGroqKey(envGroqKey) ? envGroqKey.trim() : \"\";\n memoryIpc.setGroqApiKey(resolvedGroqKey);\n if (resolvedGroqKey && savedGroqKey !== resolvedGroqKey) {\n memoryIpc.setState(\"groqApiKey\", resolvedGroqKey);\n }\n memoryIpc.registerIpcHandlers();\n memoryIpc.registerEducatorIpc();\n memoryIpc.reconcileInterruptedCourseGeneration();\n memoryIpc.registerVoiceIpc();\n memoryIpc.registerGamesIpc();\n memoryIpc.registerSyncIpc();\n memoryIpc.registerMemoryIpc();\n registerOverlayIpc(getWindow);\n mainWindow = createMainWindow();\n createTray(getWindow);\n registerHotkey(getWindow);\n initOverlay();\n memoryIpc.getMachineId();\n memoryIpc.startTelemetryLoop();\n initAutoUpdater(getWindow);\n mainWindow.on(\"minimize\", () => showOverlay());\n mainWindow.on(\"hide\", () => showOverlay());\n mainWindow.on(\"restore\", () => hideOverlay());\n mainWindow.on(\"show\", () => hideOverlay());\n mainWindow.on(\"closed\", () => {\n mainWindow = null;\n });\n electron.app.on(\"activate\", () => {\n if (electron.BrowserWindow.getAllWindows().length === 0) {\n mainWindow = createMainWindow();\n }\n });\n});\nelectron.app.on(\"will-quit\", () => {\n memoryIpc.saveDBSync();\n unregisterHotkeys();\n});\nelectron.app.on(\"window-all-closed\", () => {\n});\n"}}, "src/main/claude.ts": {"9afc3b31c06b42783c3a2594e9208c01e5127acd": {"t": 1785746552.8100712, "n": 15646, "h": "18964a8d82631861", "c": "// DeepSeek transport behind the existing Claude compatibility layer.\nconst DEFAULT_DEEPSEEK_API_URL = 'https://api.deepseek.com/chat/completions'\nconst CLAUDE_CONNECT_TIMEOUT_MS = 20_000\nconst CLAUDE_REQUEST_TIMEOUT_MS = 45_000\nconst CLAUDE_MAX_ATTEMPTS = 2\n\ntype ClaudeMessage = { role: string; content: string }\ntype ClaudeStreamChunk = { token: string; done: boolean; inputTokens?: number; outputTokens?: number }\ntype DeepSeekRole = 'system' | 'user' | 'assistant'\nexport interface ClaudeRequestOptions {\n timeoutMs?: number\n maxAttempts?: number\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null\n}\n\nfunction resolveClaudeApiUrl(): string {\n const raw = (process.env['DEEPSEEK_API_URL'] || DEFAULT_DEEPSEEK_API_URL || '').trim()\n if (!raw || raw.startsWith('sk-')) {\n return DEFAULT_DEEPSEEK_API_URL\n }\n try {\n return new URL(raw).toString()\n } catch {\n return DEFAULT_DEEPSEEK_API_URL\n }\n}\n\nfunction extractClaudeErrorCode(err: unknown): string {\n if (!isRecord(err)) return ''\n\n if (typeof err.code === 'string' && err.code) return err.code\n\n if (isRecord(err.cause)) {\n if (typeof err.cause.code === 'string' && err.cause.code) return err.cause.code\n if (typeof err.cause.name === 'string' && err.cause.name) return err.cause.name\n }\n\n return typeof err.name === 'string' ? err.name : ''\n}\n\nfunction extractClaudeErrorMessage(err: unknown): string {\n if (err instanceof Error) {\n const causeMessage = isRecord(err.cause) && typeof err.cause.message === 'string'\n ? err.cause.message\n : ''\n return causeMessage && causeMessage !== err.message ? `${err.message} (${causeMessage})` : err.message\n }\n return String(err || '')\n}\n\nfunction formatClaudeNetworkError(err: unknown): string {\n const code = extractClaudeErrorCode(err)\n const message = extractClaudeErrorMessage(err).toLowerCase()\n\n if (code === 'ENOTFOUND') {\n return 'Cannot resolve api.deepseek.com. Check DNS, VPN, or firewall settings.'\n }\n if (code === 'ECONNRESET' || code === 'UND_ERR_SOCKET') {\n return 'The connection to DeepSeek was interrupted suddenly. Try again.'\n }\n if (code === 'ETIMEDOUT' || code === 'UND_ERR_CONNECT_TIMEOUT' || code === 'AbortError' || code === 'ABORT_ERR') {\n return 'DeepSeek did not respond in time. Try again in a few seconds.'\n }\n if (code === 'ENETUNREACH' || code === 'EHOSTUNREACH') {\n return 'The local network cannot currently reach DeepSeek.'\n }\n if (code === 'CERT_HAS_EXPIRED' || code === 'SELF_SIGNED_CERT_IN_CHAIN' || code === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE') {\n return 'The TLS connection to DeepSeek failed. Check the certificate, antivirus, or HTTPS proxy.'\n }\n if (message.includes('fetch failed')) {\n return 'The connection to DeepSeek failed temporarily. Try again.'\n }\n if (message.includes('timeout') || message.includes('timed out')) {\n return 'DeepSeek is responding too slowly right now. Try again.'\n }\n\n return `The connection to DeepSeek failed: ${extractClaudeErrorMessage(err) || 'unknown error'}`\n}\n\nfunction isRetryableClaudeError(err: unknown): boolean {\n const code = extractClaudeErrorCode(err)\n if (['ENOTFOUND', 'ECONNRESET', 'ETIMEDOUT', 'EAI_AGAIN', 'UND_ERR_SOCKET', 'UND_ERR_CONNECT_TIMEOUT', 'AbortError', 'ABORT_ERR'].includes(code)) {\n return true\n }\n\n const message = extractClaudeErrorMessage(err).toLowerCase()\n return message.includes('fetch failed') || message.includes('timeout') || message.includes('timed out')\n}\n\nfunction formatClaudeLogError(err: unknown): string {\n const code = extractClaudeErrorCode(err)\n const message = extractClaudeErrorMessage(err) || 'unknown error'\n return code ? `${code}: ${message}` : message\n}\n\nfunction buildClaudeHeaders(key: string): Record<string, string> {\n return {\n Authorization: `Bearer ${key}`,\n 'content-type': 'application/json',\n }\n}\n\nasync function wait(ms: number): Promise<void> {\n await new Promise((resolve) => setTimeout(resolve, ms))\n}\n\nasync function fetchClaudeResponse(\n payload: Record<string, unknown>,\n options?: { timeoutMs?: number; maxAttempts?: number }\n): Promise<Response> {\n const key = getClaudeApiKey()\n if (!key) throw new Error('DeepSeek API key not set')\n\n const apiUrl = resolveClaudeApiUrl()\n const body = JSON.stringify(payload)\n const timeoutMs = Math.max(1_000, options?.timeoutMs ?? CLAUDE_REQUEST_TIMEOUT_MS)\n const maxAttempts = Math.max(1, options?.maxAttempts ?? CLAUDE_MAX_ATTEMPTS)\n let lastError: unknown = null\n\n for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {\n const controller = new AbortController()\n let timedOut = false\n const timeout = setTimeout(() => {\n timedOut = true\n controller.abort()\n }, timeoutMs)\n\n try {\n const response = await fetch(apiUrl, {\n method: 'POST',\n headers: buildClaudeHeaders(key),\n body,\n signal: controller.signal,\n })\n clearTimeout(timeout)\n return response\n } catch (err) {\n clearTimeout(timeout)\n const normalizedError = timedOut\n ? new Error(`DeepSeek request timed out after ${timeoutMs}ms`)\n : err\n\n lastError = normalizedError\n console.error(`[DeepSeek] request attempt ${attempt}/${maxAttempts} failed: ${formatClaudeLogError(normalizedError)}`)\n\n if (attempt >= maxAttempts || !isRetryableClaudeError(normalizedError)) {\n throw normalizedError\n }\n\n await wait(250 * attempt)\n }\n }\n\n throw (lastError instanceof Error ? lastError : new Error('DeepSeek request failed'))\n}\n\n// Kept under the existing export names to avoid rewiring renderer/main imports.\nexport const CLAUDE_CHAT_MODEL = 'deepseek-chat'\nexport const CLAUDE_CHAT_DEEP_MODEL = 'deepseek-reasoner'\nexport const CLAUDE_COURSE_MODEL = 'deepseek-chat'\nexport const CLAUDE_TEACHER_MODEL = 'deepseek-chat'\n\nconst DEEP_CHAT_PATTERN = /```|\\b(debug|bug|refactor|architecture|arhitectur|design|trade-?off|compare|critic|review|analiz|analysis|eseu|essay|proof|derive|strategy|strategie|complex|plan detaliat|de ce|why exactly)\\b/i\n\nfunction pickClaudeChatModel(messages: ClaudeMessage[], maxTokens: number): string {\n const userMessages = messages.filter((message) => message.role === 'user')\n const lastUserMessage = userMessages[userMessages.length - 1]?.content || ''\n const totalChars = userMessages.reduce((sum, message) => sum + String(message.content || '').length, 0)\n const lineCount = (lastUserMessage.match(/\\n/g) || []).length + 1\n\n let complexityScore = 0\n if (DEEP_CHAT_PATTERN.test(lastUserMessage)) complexityScore += 2\n if (lastUserMessage.length >= 320) complexityScore += 1\n if (lineCount >= 6) complexityScore += 1\n if (messages.length >= 8) complexityScore += 1\n if (totalChars >= 1_200) complexityScore += 1\n if (maxTokens > 1_200) complexityScore += 1\n\n return complexityScore >= 2 ? CLAUDE_CHAT_DEEP_MODEL : CLAUDE_CHAT_MODEL\n}\n\nexport function setClaudeApiKey(key: string) {\n ;(globalThis as { __claudeApiKey?: string }).__claudeApiKey = key\n}\n\nexport function getClaudeApiKey(): string {\n return (globalThis as { __claudeApiKey?: string }).__claudeApiKey || ''\n}\n\nfunction formatClaudeHttpError(status: number, errText: string): string {\n if (status === 401) {\n return 'Invalid or expired API key. Set a valid DeepSeek key in the app.'\n }\n if (status === 402) {\n return 'DeepSeek billing or balance is unavailable for this request. Check the account.'\n }\n if (status === 403) {\n return 'Access denied for this account or model. Check your DeepSeek plan.'\n }\n if (status === 429) {\n return 'DeepSeek is rate-limiting requests right now. Try again in a moment.'\n }\n return `DeepSeek API error: ${status} - ${errText.slice(0, 200)}`\n}\n\nfunction normalizeMessages(messages: ClaudeMessage[], systemPrompt = ''): Array<{ role: DeepSeekRole; content: string }> {\n const normalized: Array<{ role: DeepSeekRole; content: string }> = []\n\n if (systemPrompt.trim()) {\n normalized.push({ role: 'system', content: systemPrompt.trim() })\n }\n\n for (const message of messages) {\n const content = String(message.content || '').trim()\n if (!content) continue\n\n const role: DeepSeekRole = message.role === 'assistant'\n ? 'assistant'\n : message.role === 'system'\n ? 'system'\n : 'user'\n\n const last = normalized[normalized.length - 1]\n if (last && last.role === role) {\n last.content += `\\n${content}`\n } else {\n normalized.push({ role, content })\n }\n }\n\n return normalized\n}\n\nfunction extractTextPart(part: unknown): string {\n if (typeof part === 'string') return part\n if (!isRecord(part)) return ''\n return typeof part.text === 'string' ? part.text : ''\n}\n\nfunction extractMessageText(content: unknown): string {\n if (typeof content === 'string') return content\n if (Array.isArray(content)) {\n return content.map(extractTextPart).join('')\n }\n return ''\n}\n\nfunction extractResponseText(data: unknown): string {\n if (!isRecord(data) || !Array.isArray(data.choices) || data.choices.length === 0) {\n return ''\n }\n\n const firstChoice = data.choices[0]\n if (!isRecord(firstChoice) || !isRecord(firstChoice.message)) {\n return ''\n }\n\n return extractMessageText(firstChoice.message.content)\n}\n\nfunction extractDeltaText(data: unknown): string {\n if (!isRecord(data) || !Array.isArray(data.choices) || data.choices.length === 0) {\n return ''\n }\n\n const firstChoice = data.choices[0]\n if (!isRecord(firstChoice) || !isRecord(firstChoice.delta)) {\n return ''\n }\n\n return extractMessageText(firstChoice.delta.content)\n}\n\nfunction extractUsage(data: unknown): { inputTokens: number; outputTokens: number } {\n if (!isRecord(data) || !isRecord(data.usage)) {\n return { inputTokens: 0, outputTokens: 0 }\n }\n\n const usage = data.usage\n const inputTokens = typeof usage.prompt_tokens === 'number'\n ? usage.prompt_tokens\n : typeof usage.input_tokens === 'number'\n ? usage.input_tokens\n : 0\n const outputTokens = typeof usage.completion_tokens === 'number'\n ? usage.completion_tokens\n : typeof usage.output_tokens === 'number'\n ? usage.output_tokens\n : 0\n\n return { inputTokens, outputTokens }\n}\n\nexport async function checkClaudeHealth(): Promise<boolean> {\n const key = getClaudeApiKey()\n if (!key) return false\n\n try {\n const res = await fetchClaudeResponse({\n model: CLAUDE_CHAT_MODEL,\n max_tokens: 8,\n stream: false,\n messages: normalizeMessages([{ role: 'user', content: 'ping' }]),\n }, {\n timeoutMs: 8_000,\n maxAttempts: 1,\n })\n\n return res.ok\n } catch {\n return false\n }\n}\n\nexport interface ClaudeTextResult {\n text: string\n inputTokens: number\n outputTokens: number\n}\n\nexport async function generateWithClaudeWithUsage(\n systemPrompt: string,\n userMessage: string,\n maxTokens = 8192,\n model = CLAUDE_COURSE_MODEL,\n requestOptions?: ClaudeRequestOptions\n): Promise<ClaudeTextResult> {\n let res: Response\n try {\n res = await fetchClaudeResponse({\n model,\n max_tokens: maxTokens,\n stream: false,\n messages: normalizeMessages([{ role: 'user', content: userMessage }], systemPrompt),\n }, {\n timeoutMs: requestOptions?.timeoutMs ?? CLAUDE_REQUEST_TIMEOUT_MS,\n maxAttempts: requestOptions?.maxAttempts ?? CLAUDE_MAX_ATTEMPTS,\n })\n } catch (err) {\n throw new Error(formatClaudeNetworkError(err))\n }\n\n if (!res.ok) {\n const errText = await res.text().catch(() => '')\n throw new Error(formatClaudeHttpError(res.status, errText))\n }\n\n const data = await res.json()\n const usage = extractUsage(data)\n return {\n text: extractResponseText(data),\n inputTokens: usage.inputTokens,\n outputTokens: usage.outputTokens,\n }\n}\n\nexport async function generateWithClaude(\n systemPrompt: string,\n userMessage: string,\n maxTokens = 8192,\n model = CLAUDE_COURSE_MODEL,\n requestOptions?: ClaudeRequestOptions\n): Promise<string> {\n const result = await generateWithClaudeWithUsage(systemPrompt, userMessage, maxTokens, model, requestOptions)\n return result.text\n}\n\nasync function* streamDeepSeekResponse(res: Response): AsyncGenerator<ClaudeStreamChunk> {\n if (!res.body) {\n yield { token: '', done: true }\n return\n }\n\n const reader = res.body.getReader()\n const decoder = new TextDecoder()\n let buffer = ''\n let inputTokens = 0\n let outputTokens = 0\n\n try {\n while (true) {\n const { done, value } = await reader.read()\n if (done) break\n\n buffer += decoder.decode(value, { stream: true })\n const lines = buffer.split('\\n')\n buffer = lines.pop() || ''\n\n for (const line of lines) {\n const trimmed = line.trim()\n if (!trimmed.startsWith('data:')) continue\n\n const data = trimmed.slice(5).trim()\n if (!data) continue\n if (data === '[DONE]') {\n yield { token: '', done: true, inputTokens, outputTokens }\n return\n }\n\n try {\n const json = JSON.parse(data)\n const deltaText = extractDeltaText(json)\n if (deltaText) {\n yield { token: deltaText, done: false }\n }\n\n const usage = extractUsage(json)\n if (usage.inputTokens > 0) inputTokens = usage.inputTokens\n if (usage.outputTokens > 0) outputTokens = usage.outputTokens\n } catch {\n // Skip malformed SSE fragments.\n }\n }\n }\n } catch (err) {\n yield { token: formatClaudeNetworkError(err), done: true, inputTokens, outputTokens }\n return\n }\n\n yield { token: '', done: true, inputTokens, outputTokens }\n}\n\nexport async function* streamClaude(\n systemPrompt: string,\n userMessage: string,\n maxTokens = 4096,\n model = CLAUDE_TEACHER_MODEL,\n requestOptions?: ClaudeRequestOptions\n): AsyncGenerator<ClaudeStreamChunk> {\n if (!getClaudeApiKey()) {\n yield { token: 'DeepSeek API key not configured.', done: true }\n return\n }\n\n let res: Response\n try {\n res = await fetchClaudeResponse({\n model,\n max_tokens: maxTokens,\n stream: true,\n messages: normalizeMessages([{ role: 'user', content: userMessage }], systemPrompt),\n }, {\n timeoutMs: requestOptions?.timeoutMs ?? CLAUDE_CONNECT_TIMEOUT_MS,\n maxAttempts: requestOptions?.maxAttempts ?? CLAUDE_MAX_ATTEMPTS,\n })\n } catch (err) {\n yield { token: formatClaudeNetworkError(err), done: true }\n return\n }\n\n if (!res.ok || !res.body) {\n const errText = await res.text().catch(() => '')\n yield { token: formatClaudeHttpError(res.status, errText), done: true }\n return\n }\n\n yield* streamDeepSeekResponse(res)\n}\n\nexport async function* streamClaudeChat(\n messages: ClaudeMessage[],\n systemPrompt: string,\n maxTokens = 1024\n): AsyncGenerator<ClaudeStreamChunk> {\n if (!getClaudeApiKey()) {\n yield { token: 'API key not configured. Enter a DeepSeek key in Settings.', done: true }\n return\n }\n\n const trimmedMessages = messages.slice(-20)\n const model = pickClaudeChatModel(trimmedMessages, maxTokens)\n\n let res: Response\n try {\n res = await fetchClaudeResponse({\n model,\n max_tokens: maxTokens,\n stream: true,\n messages: normalizeMessages(trimmedMessages, systemPrompt),\n }, {\n timeoutMs: CLAUDE_CONNECT_TIMEOUT_MS,\n maxAttempts: CLAUDE_MAX_ATTEMPTS,\n })\n } catch (err) {\n yield { token: formatClaudeNetworkError(err), done: true }\n return\n }\n\n if (!res.ok || !res.body) {\n const errText = await res.text().catch(() => '')\n yield { token: formatClaudeHttpError(res.status, errText), done: true }\n return\n }\n\n yield* streamDeepSeekResponse(res)\n}\n"}, "214bb931cf283b405642479fe073947e67eb0068": {"t": 1785754165.376823, "n": 15646, "h": "18964a8d82631861", "c": "// DeepSeek transport behind the existing Claude compatibility layer.\nconst DEFAULT_DEEPSEEK_API_URL = 'https://api.deepseek.com/chat/completions'\nconst CLAUDE_CONNECT_TIMEOUT_MS = 20_000\nconst CLAUDE_REQUEST_TIMEOUT_MS = 45_000\nconst CLAUDE_MAX_ATTEMPTS = 2\n\ntype ClaudeMessage = { role: string; content: string }\ntype ClaudeStreamChunk = { token: string; done: boolean; inputTokens?: number; outputTokens?: number }\ntype DeepSeekRole = 'system' | 'user' | 'assistant'\nexport interface ClaudeRequestOptions {\n timeoutMs?: number\n maxAttempts?: number\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null\n}\n\nfunction resolveClaudeApiUrl(): string {\n const raw = (process.env['DEEPSEEK_API_URL'] || DEFAULT_DEEPSEEK_API_URL || '').trim()\n if (!raw || raw.startsWith('sk-')) {\n return DEFAULT_DEEPSEEK_API_URL\n }\n try {\n return new URL(raw).toString()\n } catch {\n return DEFAULT_DEEPSEEK_API_URL\n }\n}\n\nfunction extractClaudeErrorCode(err: unknown): string {\n if (!isRecord(err)) return ''\n\n if (typeof err.code === 'string' && err.code) return err.code\n\n if (isRecord(err.cause)) {\n if (typeof err.cause.code === 'string' && err.cause.code) return err.cause.code\n if (typeof err.cause.name === 'string' && err.cause.name) return err.cause.name\n }\n\n return typeof err.name === 'string' ? err.name : ''\n}\n\nfunction extractClaudeErrorMessage(err: unknown): string {\n if (err instanceof Error) {\n const causeMessage = isRecord(err.cause) && typeof err.cause.message === 'string'\n ? err.cause.message\n : ''\n return causeMessage && causeMessage !== err.message ? `${err.message} (${causeMessage})` : err.message\n }\n return String(err || '')\n}\n\nfunction formatClaudeNetworkError(err: unknown): string {\n const code = extractClaudeErrorCode(err)\n const message = extractClaudeErrorMessage(err).toLowerCase()\n\n if (code === 'ENOTFOUND') {\n return 'Cannot resolve api.deepseek.com. Check DNS, VPN, or firewall settings.'\n }\n if (code === 'ECONNRESET' || code === 'UND_ERR_SOCKET') {\n return 'The connection to DeepSeek was interrupted suddenly. Try again.'\n }\n if (code === 'ETIMEDOUT' || code === 'UND_ERR_CONNECT_TIMEOUT' || code === 'AbortError' || code === 'ABORT_ERR') {\n return 'DeepSeek did not respond in time. Try again in a few seconds.'\n }\n if (code === 'ENETUNREACH' || code === 'EHOSTUNREACH') {\n return 'The local network cannot currently reach DeepSeek.'\n }\n if (code === 'CERT_HAS_EXPIRED' || code === 'SELF_SIGNED_CERT_IN_CHAIN' || code === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE') {\n return 'The TLS connection to DeepSeek failed. Check the certificate, antivirus, or HTTPS proxy.'\n }\n if (message.includes('fetch failed')) {\n return 'The connection to DeepSeek failed temporarily. Try again.'\n }\n if (message.includes('timeout') || message.includes('timed out')) {\n return 'DeepSeek is responding too slowly right now. Try again.'\n }\n\n return `The connection to DeepSeek failed: ${extractClaudeErrorMessage(err) || 'unknown error'}`\n}\n\nfunction isRetryableClaudeError(err: unknown): boolean {\n const code = extractClaudeErrorCode(err)\n if (['ENOTFOUND', 'ECONNRESET', 'ETIMEDOUT', 'EAI_AGAIN', 'UND_ERR_SOCKET', 'UND_ERR_CONNECT_TIMEOUT', 'AbortError', 'ABORT_ERR'].includes(code)) {\n return true\n }\n\n const message = extractClaudeErrorMessage(err).toLowerCase()\n return message.includes('fetch failed') || message.includes('timeout') || message.includes('timed out')\n}\n\nfunction formatClaudeLogError(err: unknown): string {\n const code = extractClaudeErrorCode(err)\n const message = extractClaudeErrorMessage(err) || 'unknown error'\n return code ? `${code}: ${message}` : message\n}\n\nfunction buildClaudeHeaders(key: string): Record<string, string> {\n return {\n Authorization: `Bearer ${key}`,\n 'content-type': 'application/json',\n }\n}\n\nasync function wait(ms: number): Promise<void> {\n await new Promise((resolve) => setTimeout(resolve, ms))\n}\n\nasync function fetchClaudeResponse(\n payload: Record<string, unknown>,\n options?: { timeoutMs?: number; maxAttempts?: number }\n): Promise<Response> {\n const key = getClaudeApiKey()\n if (!key) throw new Error('DeepSeek API key not set')\n\n const apiUrl = resolveClaudeApiUrl()\n const body = JSON.stringify(payload)\n const timeoutMs = Math.max(1_000, options?.timeoutMs ?? CLAUDE_REQUEST_TIMEOUT_MS)\n const maxAttempts = Math.max(1, options?.maxAttempts ?? CLAUDE_MAX_ATTEMPTS)\n let lastError: unknown = null\n\n for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {\n const controller = new AbortController()\n let timedOut = false\n const timeout = setTimeout(() => {\n timedOut = true\n controller.abort()\n }, timeoutMs)\n\n try {\n const response = await fetch(apiUrl, {\n method: 'POST',\n headers: buildClaudeHeaders(key),\n body,\n signal: controller.signal,\n })\n clearTimeout(timeout)\n return response\n } catch (err) {\n clearTimeout(timeout)\n const normalizedError = timedOut\n ? new Error(`DeepSeek request timed out after ${timeoutMs}ms`)\n : err\n\n lastError = normalizedError\n console.error(`[DeepSeek] request attempt ${attempt}/${maxAttempts} failed: ${formatClaudeLogError(normalizedError)}`)\n\n if (attempt >= maxAttempts || !isRetryableClaudeError(normalizedError)) {\n throw normalizedError\n }\n\n await wait(250 * attempt)\n }\n }\n\n throw (lastError instanceof Error ? lastError : new Error('DeepSeek request failed'))\n}\n\n// Kept under the existing export names to avoid rewiring renderer/main imports.\nexport const CLAUDE_CHAT_MODEL = 'deepseek-chat'\nexport const CLAUDE_CHAT_DEEP_MODEL = 'deepseek-reasoner'\nexport const CLAUDE_COURSE_MODEL = 'deepseek-chat'\nexport const CLAUDE_TEACHER_MODEL = 'deepseek-chat'\n\nconst DEEP_CHAT_PATTERN = /```|\\b(debug|bug|refactor|architecture|arhitectur|design|trade-?off|compare|critic|review|analiz|analysis|eseu|essay|proof|derive|strategy|strategie|complex|plan detaliat|de ce|why exactly)\\b/i\n\nfunction pickClaudeChatModel(messages: ClaudeMessage[], maxTokens: number): string {\n const userMessages = messages.filter((message) => message.role === 'user')\n const lastUserMessage = userMessages[userMessages.length - 1]?.content || ''\n const totalChars = userMessages.reduce((sum, message) => sum + String(message.content || '').length, 0)\n const lineCount = (lastUserMessage.match(/\\n/g) || []).length + 1\n\n let complexityScore = 0\n if (DEEP_CHAT_PATTERN.test(lastUserMessage)) complexityScore += 2\n if (lastUserMessage.length >= 320) complexityScore += 1\n if (lineCount >= 6) complexityScore += 1\n if (messages.length >= 8) complexityScore += 1\n if (totalChars >= 1_200) complexityScore += 1\n if (maxTokens > 1_200) complexityScore += 1\n\n return complexityScore >= 2 ? CLAUDE_CHAT_DEEP_MODEL : CLAUDE_CHAT_MODEL\n}\n\nexport function setClaudeApiKey(key: string) {\n ;(globalThis as { __claudeApiKey?: string }).__claudeApiKey = key\n}\n\nexport function getClaudeApiKey(): string {\n return (globalThis as { __claudeApiKey?: string }).__claudeApiKey || ''\n}\n\nfunction formatClaudeHttpError(status: number, errText: string): string {\n if (status === 401) {\n return 'Invalid or expired API key. Set a valid DeepSeek key in the app.'\n }\n if (status === 402) {\n return 'DeepSeek billing or balance is unavailable for this request. Check the account.'\n }\n if (status === 403) {\n return 'Access denied for this account or model. Check your DeepSeek plan.'\n }\n if (status === 429) {\n return 'DeepSeek is rate-limiting requests right now. Try again in a moment.'\n }\n return `DeepSeek API error: ${status} - ${errText.slice(0, 200)}`\n}\n\nfunction normalizeMessages(messages: ClaudeMessage[], systemPrompt = ''): Array<{ role: DeepSeekRole; content: string }> {\n const normalized: Array<{ role: DeepSeekRole; content: string }> = []\n\n if (systemPrompt.trim()) {\n normalized.push({ role: 'system', content: systemPrompt.trim() })\n }\n\n for (const message of messages) {\n const content = String(message.content || '').trim()\n if (!content) continue\n\n const role: DeepSeekRole = message.role === 'assistant'\n ? 'assistant'\n : message.role === 'system'\n ? 'system'\n : 'user'\n\n const last = normalized[normalized.length - 1]\n if (last && last.role === role) {\n last.content += `\\n${content}`\n } else {\n normalized.push({ role, content })\n }\n }\n\n return normalized\n}\n\nfunction extractTextPart(part: unknown): string {\n if (typeof part === 'string') return part\n if (!isRecord(part)) return ''\n return typeof part.text === 'string' ? part.text : ''\n}\n\nfunction extractMessageText(content: unknown): string {\n if (typeof content === 'string') return content\n if (Array.isArray(content)) {\n return content.map(extractTextPart).join('')\n }\n return ''\n}\n\nfunction extractResponseText(data: unknown): string {\n if (!isRecord(data) || !Array.isArray(data.choices) || data.choices.length === 0) {\n return ''\n }\n\n const firstChoice = data.choices[0]\n if (!isRecord(firstChoice) || !isRecord(firstChoice.message)) {\n return ''\n }\n\n return extractMessageText(firstChoice.message.content)\n}\n\nfunction extractDeltaText(data: unknown): string {\n if (!isRecord(data) || !Array.isArray(data.choices) || data.choices.length === 0) {\n return ''\n }\n\n const firstChoice = data.choices[0]\n if (!isRecord(firstChoice) || !isRecord(firstChoice.delta)) {\n return ''\n }\n\n return extractMessageText(firstChoice.delta.content)\n}\n\nfunction extractUsage(data: unknown): { inputTokens: number; outputTokens: number } {\n if (!isRecord(data) || !isRecord(data.usage)) {\n return { inputTokens: 0, outputTokens: 0 }\n }\n\n const usage = data.usage\n const inputTokens = typeof usage.prompt_tokens === 'number'\n ? usage.prompt_tokens\n : typeof usage.input_tokens === 'number'\n ? usage.input_tokens\n : 0\n const outputTokens = typeof usage.completion_tokens === 'number'\n ? usage.completion_tokens\n : typeof usage.output_tokens === 'number'\n ? usage.output_tokens\n : 0\n\n return { inputTokens, outputTokens }\n}\n\nexport async function checkClaudeHealth(): Promise<boolean> {\n const key = getClaudeApiKey()\n if (!key) return false\n\n try {\n const res = await fetchClaudeResponse({\n model: CLAUDE_CHAT_MODEL,\n max_tokens: 8,\n stream: false,\n messages: normalizeMessages([{ role: 'user', content: 'ping' }]),\n }, {\n timeoutMs: 8_000,\n maxAttempts: 1,\n })\n\n return res.ok\n } catch {\n return false\n }\n}\n\nexport interface ClaudeTextResult {\n text: string\n inputTokens: number\n outputTokens: number\n}\n\nexport async function generateWithClaudeWithUsage(\n systemPrompt: string,\n userMessage: string,\n maxTokens = 8192,\n model = CLAUDE_COURSE_MODEL,\n requestOptions?: ClaudeRequestOptions\n): Promise<ClaudeTextResult> {\n let res: Response\n try {\n res = await fetchClaudeResponse({\n model,\n max_tokens: maxTokens,\n stream: false,\n messages: normalizeMessages([{ role: 'user', content: userMessage }], systemPrompt),\n }, {\n timeoutMs: requestOptions?.timeoutMs ?? CLAUDE_REQUEST_TIMEOUT_MS,\n maxAttempts: requestOptions?.maxAttempts ?? CLAUDE_MAX_ATTEMPTS,\n })\n } catch (err) {\n throw new Error(formatClaudeNetworkError(err))\n }\n\n if (!res.ok) {\n const errText = await res.text().catch(() => '')\n throw new Error(formatClaudeHttpError(res.status, errText))\n }\n\n const data = await res.json()\n const usage = extractUsage(data)\n return {\n text: extractResponseText(data),\n inputTokens: usage.inputTokens,\n outputTokens: usage.outputTokens,\n }\n}\n\nexport async function generateWithClaude(\n systemPrompt: string,\n userMessage: string,\n maxTokens = 8192,\n model = CLAUDE_COURSE_MODEL,\n requestOptions?: ClaudeRequestOptions\n): Promise<string> {\n const result = await generateWithClaudeWithUsage(systemPrompt, userMessage, maxTokens, model, requestOptions)\n return result.text\n}\n\nasync function* streamDeepSeekResponse(res: Response): AsyncGenerator<ClaudeStreamChunk> {\n if (!res.body) {\n yield { token: '', done: true }\n return\n }\n\n const reader = res.body.getReader()\n const decoder = new TextDecoder()\n let buffer = ''\n let inputTokens = 0\n let outputTokens = 0\n\n try {\n while (true) {\n const { done, value } = await reader.read()\n if (done) break\n\n buffer += decoder.decode(value, { stream: true })\n const lines = buffer.split('\\n')\n buffer = lines.pop() || ''\n\n for (const line of lines) {\n const trimmed = line.trim()\n if (!trimmed.startsWith('data:')) continue\n\n const data = trimmed.slice(5).trim()\n if (!data) continue\n if (data === '[DONE]') {\n yield { token: '', done: true, inputTokens, outputTokens }\n return\n }\n\n try {\n const json = JSON.parse(data)\n const deltaText = extractDeltaText(json)\n if (deltaText) {\n yield { token: deltaText, done: false }\n }\n\n const usage = extractUsage(json)\n if (usage.inputTokens > 0) inputTokens = usage.inputTokens\n if (usage.outputTokens > 0) outputTokens = usage.outputTokens\n } catch {\n // Skip malformed SSE fragments.\n }\n }\n }\n } catch (err) {\n yield { token: formatClaudeNetworkError(err), done: true, inputTokens, outputTokens }\n return\n }\n\n yield { token: '', done: true, inputTokens, outputTokens }\n}\n\nexport async function* streamClaude(\n systemPrompt: string,\n userMessage: string,\n maxTokens = 4096,\n model = CLAUDE_TEACHER_MODEL,\n requestOptions?: ClaudeRequestOptions\n): AsyncGenerator<ClaudeStreamChunk> {\n if (!getClaudeApiKey()) {\n yield { token: 'DeepSeek API key not configured.', done: true }\n return\n }\n\n let res: Response\n try {\n res = await fetchClaudeResponse({\n model,\n max_tokens: maxTokens,\n stream: true,\n messages: normalizeMessages([{ role: 'user', content: userMessage }], systemPrompt),\n }, {\n timeoutMs: requestOptions?.timeoutMs ?? CLAUDE_CONNECT_TIMEOUT_MS,\n maxAttempts: requestOptions?.maxAttempts ?? CLAUDE_MAX_ATTEMPTS,\n })\n } catch (err) {\n yield { token: formatClaudeNetworkError(err), done: true }\n return\n }\n\n if (!res.ok || !res.body) {\n const errText = await res.text().catch(() => '')\n yield { token: formatClaudeHttpError(res.status, errText), done: true }\n return\n }\n\n yield* streamDeepSeekResponse(res)\n}\n\nexport async function* streamClaudeChat(\n messages: ClaudeMessage[],\n systemPrompt: string,\n maxTokens = 1024\n): AsyncGenerator<ClaudeStreamChunk> {\n if (!getClaudeApiKey()) {\n yield { token: 'API key not configured. Enter a DeepSeek key in Settings.', done: true }\n return\n }\n\n const trimmedMessages = messages.slice(-20)\n const model = pickClaudeChatModel(trimmedMessages, maxTokens)\n\n let res: Response\n try {\n res = await fetchClaudeResponse({\n model,\n max_tokens: maxTokens,\n stream: true,\n messages: normalizeMessages(trimmedMessages, systemPrompt),\n }, {\n timeoutMs: CLAUDE_CONNECT_TIMEOUT_MS,\n maxAttempts: CLAUDE_MAX_ATTEMPTS,\n })\n } catch (err) {\n yield { token: formatClaudeNetworkError(err), done: true }\n return\n }\n\n if (!res.ok || !res.body) {\n const errText = await res.text().catch(() => '')\n yield { token: formatClaudeHttpError(res.status, errText), done: true }\n return\n }\n\n yield* streamDeepSeekResponse(res)\n}\n"}}}} |