Compare commits
No commits in common. "2ebde60dcd72332b648a58bf480b0d3e6e3610f8" and "1b7496800605ef5d086256466d925bfe502dcd40" have entirely different histories.
2ebde60dcd
...
1b74968006
151
forge.config.js
151
forge.config.js
@ -1,83 +1,78 @@
|
|||||||
const { FusesPlugin } = require("@electron-forge/plugin-fuses");
|
const { FusesPlugin } = require('@electron-forge/plugin-fuses');
|
||||||
const { FuseV1Options, FuseVersion } = require("@electron/fuses");
|
const { FuseV1Options, FuseVersion } = require('@electron/fuses');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
packagerConfig: {
|
packagerConfig: {
|
||||||
asar: {
|
asar: {
|
||||||
unpack:
|
unpack: '**/{onnxruntime-node,onnxruntime-common,@huggingface/transformers,sharp,@img}/**',
|
||||||
"**/{onnxruntime-node,onnxruntime-common,@huggingface/transformers,sharp,@img}/**",
|
|
||||||
},
|
|
||||||
extraResource: ["./src/assets/SystemAudioDump"],
|
|
||||||
name: "Mastermind",
|
|
||||||
icon: "src/assets/logo",
|
|
||||||
// use `security find-identity -v -p codesigning` to find your identity
|
|
||||||
// for macos signing
|
|
||||||
// also fuck apple
|
|
||||||
// osxSign: {
|
|
||||||
// identity: '<paste your identity here>',
|
|
||||||
// optionsForFile: (filePath) => {
|
|
||||||
// return {
|
|
||||||
// entitlements: 'entitlements.plist',
|
|
||||||
// };
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// notarize if off cuz i ran this for 6 hours and it still didnt finish
|
|
||||||
// osxNotarize: {
|
|
||||||
// appleId: 'your apple id',
|
|
||||||
// appleIdPassword: 'app specific password',
|
|
||||||
// teamId: 'your team id',
|
|
||||||
// },
|
|
||||||
},
|
|
||||||
rebuildConfig: {
|
|
||||||
// Ensure onnxruntime-node is rebuilt against Electron's Node.js headers
|
|
||||||
// so the native binding matches the ABI used in packaged builds.
|
|
||||||
onlyModules: ["onnxruntime-node", "sharp"],
|
|
||||||
},
|
|
||||||
makers: [
|
|
||||||
{
|
|
||||||
name: "@electron-forge/maker-squirrel",
|
|
||||||
config: {
|
|
||||||
name: "mastermind",
|
|
||||||
productName: "Mastermind",
|
|
||||||
shortcutName: "Mastermind",
|
|
||||||
createDesktopShortcut: true,
|
|
||||||
createStartMenuShortcut: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "@electron-forge/maker-dmg",
|
|
||||||
platforms: ["darwin"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "@reforged/maker-appimage",
|
|
||||||
platforms: ["linux"],
|
|
||||||
config: {
|
|
||||||
options: {
|
|
||||||
name: "Mastermind",
|
|
||||||
productName: "Mastermind",
|
|
||||||
genericName: "AI Assistant",
|
|
||||||
description: "AI assistant for interviews and learning",
|
|
||||||
categories: ["Development", "Education"],
|
|
||||||
icon: "src/assets/logo.png",
|
|
||||||
},
|
},
|
||||||
},
|
extraResource: ['./src/assets/SystemAudioDump'],
|
||||||
|
name: 'Mastermind',
|
||||||
|
icon: 'src/assets/logo',
|
||||||
|
// use `security find-identity -v -p codesigning` to find your identity
|
||||||
|
// for macos signing
|
||||||
|
// also fuck apple
|
||||||
|
// osxSign: {
|
||||||
|
// identity: '<paste your identity here>',
|
||||||
|
// optionsForFile: (filePath) => {
|
||||||
|
// return {
|
||||||
|
// entitlements: 'entitlements.plist',
|
||||||
|
// };
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// notarize if off cuz i ran this for 6 hours and it still didnt finish
|
||||||
|
// osxNotarize: {
|
||||||
|
// appleId: 'your apple id',
|
||||||
|
// appleIdPassword: 'app specific password',
|
||||||
|
// teamId: 'your team id',
|
||||||
|
// },
|
||||||
},
|
},
|
||||||
],
|
rebuildConfig: {},
|
||||||
plugins: [
|
makers: [
|
||||||
{
|
{
|
||||||
name: "@electron-forge/plugin-auto-unpack-natives",
|
name: '@electron-forge/maker-squirrel',
|
||||||
config: {},
|
config: {
|
||||||
},
|
name: 'mastermind',
|
||||||
// Fuses are used to enable/disable various Electron functionality
|
productName: 'Mastermind',
|
||||||
// at package time, before code signing the application
|
shortcutName: 'Mastermind',
|
||||||
new FusesPlugin({
|
createDesktopShortcut: true,
|
||||||
version: FuseVersion.V1,
|
createStartMenuShortcut: true,
|
||||||
[FuseV1Options.RunAsNode]: false,
|
},
|
||||||
[FuseV1Options.EnableCookieEncryption]: true,
|
},
|
||||||
[FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false,
|
{
|
||||||
[FuseV1Options.EnableNodeCliInspectArguments]: false,
|
name: '@electron-forge/maker-dmg',
|
||||||
[FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true,
|
platforms: ['darwin'],
|
||||||
[FuseV1Options.OnlyLoadAppFromAsar]: true,
|
},
|
||||||
}),
|
{
|
||||||
],
|
name: '@reforged/maker-appimage',
|
||||||
|
platforms: ['linux'],
|
||||||
|
config: {
|
||||||
|
options: {
|
||||||
|
name: 'Mastermind',
|
||||||
|
productName: 'Mastermind',
|
||||||
|
genericName: 'AI Assistant',
|
||||||
|
description: 'AI assistant for interviews and learning',
|
||||||
|
categories: ['Development', 'Education'],
|
||||||
|
icon: 'src/assets/logo.png'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
plugins: [
|
||||||
|
{
|
||||||
|
name: '@electron-forge/plugin-auto-unpack-natives',
|
||||||
|
config: {},
|
||||||
|
},
|
||||||
|
// Fuses are used to enable/disable various Electron functionality
|
||||||
|
// at package time, before code signing the application
|
||||||
|
new FusesPlugin({
|
||||||
|
version: FuseVersion.V1,
|
||||||
|
[FuseV1Options.RunAsNode]: false,
|
||||||
|
[FuseV1Options.EnableCookieEncryption]: true,
|
||||||
|
[FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false,
|
||||||
|
[FuseV1Options.EnableNodeCliInspectArguments]: false,
|
||||||
|
[FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true,
|
||||||
|
[FuseV1Options.OnlyLoadAppFromAsar]: true,
|
||||||
|
}),
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
102
package.json
102
package.json
@ -1,55 +1,53 @@
|
|||||||
{
|
{
|
||||||
"name": "mastermind",
|
"name": "mastermind",
|
||||||
"productName": "Mastermind",
|
"productName": "Mastermind",
|
||||||
"version": "0.7.0",
|
"version": "0.7.0",
|
||||||
"description": "Mastermind AI assistant",
|
"description": "Mastermind AI assistant",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "electron-forge start",
|
"start": "electron-forge start",
|
||||||
"package": "electron-forge package",
|
"package": "electron-forge package",
|
||||||
"make": "electron-forge make",
|
"make": "electron-forge make",
|
||||||
"publish": "electron-forge publish",
|
"publish": "electron-forge publish",
|
||||||
"lint": "echo \"No linting configured\"",
|
"lint": "echo \"No linting configured\""
|
||||||
"postinstall": "electron-rebuild -f -w onnxruntime-node"
|
},
|
||||||
},
|
"keywords": [
|
||||||
"keywords": [
|
"mastermind",
|
||||||
"mastermind",
|
"mastermind ai",
|
||||||
"mastermind ai",
|
"mastermind ai assistant",
|
||||||
"mastermind ai assistant",
|
"mastermind ai assistant for interviews",
|
||||||
"mastermind ai assistant for interviews",
|
"mastermind ai assistant for interviews"
|
||||||
"mastermind ai assistant for interviews"
|
],
|
||||||
],
|
"author": {
|
||||||
"author": {
|
"name": "ShiftyX1",
|
||||||
"name": "ShiftyX1",
|
"email": "lead@pyserve.org"
|
||||||
"email": "lead@pyserve.org"
|
},
|
||||||
},
|
"license": "GPL-3.0",
|
||||||
"license": "GPL-3.0",
|
"dependencies": {
|
||||||
"dependencies": {
|
"@google/genai": "^1.41.0",
|
||||||
"@google/genai": "^1.41.0",
|
"@huggingface/transformers": "^3.8.1",
|
||||||
"@huggingface/transformers": "^3.8.1",
|
"electron-squirrel-startup": "^1.0.1",
|
||||||
"electron-squirrel-startup": "^1.0.1",
|
"ollama": "^0.6.3",
|
||||||
"ollama": "^0.6.3",
|
"openai": "^6.22.0",
|
||||||
"openai": "^6.22.0",
|
"p-retry": "^4.6.2",
|
||||||
"p-retry": "^4.6.2",
|
"ws": "^8.19.0"
|
||||||
"ws": "^8.19.0"
|
},
|
||||||
},
|
"devDependencies": {
|
||||||
"devDependencies": {
|
"@electron-forge/cli": "^7.8.1",
|
||||||
"@electron/rebuild": "^3.7.1",
|
"@electron-forge/maker-deb": "^7.8.1",
|
||||||
"@electron-forge/cli": "^7.8.1",
|
"@electron-forge/maker-dmg": "^7.8.1",
|
||||||
"@electron-forge/maker-deb": "^7.8.1",
|
"@electron-forge/maker-rpm": "^7.8.1",
|
||||||
"@electron-forge/maker-dmg": "^7.8.1",
|
"@electron-forge/maker-squirrel": "^7.8.1",
|
||||||
"@electron-forge/maker-rpm": "^7.8.1",
|
"@electron-forge/maker-zip": "^7.8.1",
|
||||||
"@electron-forge/maker-squirrel": "^7.8.1",
|
"@electron-forge/plugin-auto-unpack-natives": "^7.8.1",
|
||||||
"@electron-forge/maker-zip": "^7.8.1",
|
"@electron-forge/plugin-fuses": "^7.8.1",
|
||||||
"@electron-forge/plugin-auto-unpack-natives": "^7.8.1",
|
"@electron/fuses": "^1.8.0",
|
||||||
"@electron-forge/plugin-fuses": "^7.8.1",
|
"@reforged/maker-appimage": "^5.0.0",
|
||||||
"@electron/fuses": "^1.8.0",
|
"electron": "^30.0.5"
|
||||||
"@reforged/maker-appimage": "^5.0.0",
|
},
|
||||||
"electron": "^30.0.5"
|
"pnpm": {
|
||||||
},
|
"overrides": {
|
||||||
"pnpm": {
|
"p-retry": "4.6.2"
|
||||||
"overrides": {
|
}
|
||||||
"p-retry": "4.6.2"
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -382,7 +382,6 @@ export class CheatingDaddyApp extends LitElement {
|
|||||||
_storageLoaded: { state: true },
|
_storageLoaded: { state: true },
|
||||||
_updateAvailable: { state: true },
|
_updateAvailable: { state: true },
|
||||||
_whisperDownloading: { state: true },
|
_whisperDownloading: { state: true },
|
||||||
_whisperProgress: { state: true },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -408,7 +407,6 @@ export class CheatingDaddyApp extends LitElement {
|
|||||||
this._timerInterval = null;
|
this._timerInterval = null;
|
||||||
this._updateAvailable = false;
|
this._updateAvailable = false;
|
||||||
this._whisperDownloading = false;
|
this._whisperDownloading = false;
|
||||||
this._whisperProgress = null;
|
|
||||||
this._localVersion = "";
|
this._localVersion = "";
|
||||||
|
|
||||||
this._loadFromStorage();
|
this._loadFromStorage();
|
||||||
@ -487,10 +485,6 @@ export class CheatingDaddyApp extends LitElement {
|
|||||||
);
|
);
|
||||||
ipcRenderer.on("whisper-downloading", (_, downloading) => {
|
ipcRenderer.on("whisper-downloading", (_, downloading) => {
|
||||||
this._whisperDownloading = downloading;
|
this._whisperDownloading = downloading;
|
||||||
if (!downloading) this._whisperProgress = null;
|
|
||||||
});
|
|
||||||
ipcRenderer.on("whisper-progress", (_, progress) => {
|
|
||||||
this._whisperProgress = progress;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -506,7 +500,6 @@ export class CheatingDaddyApp extends LitElement {
|
|||||||
ipcRenderer.removeAllListeners("click-through-toggled");
|
ipcRenderer.removeAllListeners("click-through-toggled");
|
||||||
ipcRenderer.removeAllListeners("reconnect-failed");
|
ipcRenderer.removeAllListeners("reconnect-failed");
|
||||||
ipcRenderer.removeAllListeners("whisper-downloading");
|
ipcRenderer.removeAllListeners("whisper-downloading");
|
||||||
ipcRenderer.removeAllListeners("whisper-progress");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -785,7 +778,6 @@ export class CheatingDaddyApp extends LitElement {
|
|||||||
.onStart=${() => this.handleStart()}
|
.onStart=${() => this.handleStart()}
|
||||||
.onExternalLink=${(url) => this.handleExternalLinkClick(url)}
|
.onExternalLink=${(url) => this.handleExternalLinkClick(url)}
|
||||||
.whisperDownloading=${this._whisperDownloading}
|
.whisperDownloading=${this._whisperDownloading}
|
||||||
.whisperProgress=${this._whisperProgress}
|
|
||||||
></main-view>
|
></main-view>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
547
src/index.js
547
src/index.js
@ -1,336 +1,299 @@
|
|||||||
if (require("electron-squirrel-startup")) {
|
if (require('electron-squirrel-startup')) {
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Global crash handlers to prevent silent process termination ──
|
const { app, BrowserWindow, shell, ipcMain } = require('electron');
|
||||||
process.on("uncaughtException", (error) => {
|
const { createWindow, updateGlobalShortcuts } = require('./utils/window');
|
||||||
console.error("[FATAL] Uncaught exception:", error);
|
const { setupGeminiIpcHandlers, stopMacOSAudioCapture, sendToRenderer } = require('./utils/gemini');
|
||||||
try {
|
const storage = require('./storage');
|
||||||
const { sendToRenderer } = require("./utils/gemini");
|
|
||||||
sendToRenderer(
|
|
||||||
"update-status",
|
|
||||||
"Fatal error: " + (error?.message || "unknown"),
|
|
||||||
);
|
|
||||||
} catch (_) {
|
|
||||||
// sendToRenderer may not be available yet
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
process.on("unhandledRejection", (reason) => {
|
|
||||||
console.error("[FATAL] Unhandled promise rejection:", reason);
|
|
||||||
try {
|
|
||||||
const { sendToRenderer } = require("./utils/gemini");
|
|
||||||
sendToRenderer(
|
|
||||||
"update-status",
|
|
||||||
"Unhandled error: " +
|
|
||||||
(reason instanceof Error ? reason.message : String(reason)),
|
|
||||||
);
|
|
||||||
} catch (_) {
|
|
||||||
// sendToRenderer may not be available yet
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const { app, BrowserWindow, shell, ipcMain } = require("electron");
|
|
||||||
const { createWindow, updateGlobalShortcuts } = require("./utils/window");
|
|
||||||
const {
|
|
||||||
setupGeminiIpcHandlers,
|
|
||||||
stopMacOSAudioCapture,
|
|
||||||
sendToRenderer,
|
|
||||||
} = require("./utils/gemini");
|
|
||||||
const storage = require("./storage");
|
|
||||||
|
|
||||||
const geminiSessionRef = { current: null };
|
const geminiSessionRef = { current: null };
|
||||||
let mainWindow = null;
|
let mainWindow = null;
|
||||||
|
|
||||||
function createMainWindow() {
|
function createMainWindow() {
|
||||||
mainWindow = createWindow(sendToRenderer, geminiSessionRef);
|
mainWindow = createWindow(sendToRenderer, geminiSessionRef);
|
||||||
return mainWindow;
|
return mainWindow;
|
||||||
}
|
}
|
||||||
|
|
||||||
app.whenReady().then(async () => {
|
app.whenReady().then(async () => {
|
||||||
// Initialize storage (checks version, resets if needed)
|
// Initialize storage (checks version, resets if needed)
|
||||||
storage.initializeStorage();
|
storage.initializeStorage();
|
||||||
|
|
||||||
// Trigger screen recording permission prompt on macOS if not already granted
|
// Trigger screen recording permission prompt on macOS if not already granted
|
||||||
if (process.platform === "darwin") {
|
if (process.platform === 'darwin') {
|
||||||
const { desktopCapturer } = require("electron");
|
const { desktopCapturer } = require('electron');
|
||||||
desktopCapturer.getSources({ types: ["screen"] }).catch(() => {});
|
desktopCapturer.getSources({ types: ['screen'] }).catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
createMainWindow();
|
|
||||||
setupGeminiIpcHandlers(geminiSessionRef);
|
|
||||||
setupStorageIpcHandlers();
|
|
||||||
setupGeneralIpcHandlers();
|
|
||||||
});
|
|
||||||
|
|
||||||
app.on("window-all-closed", () => {
|
|
||||||
stopMacOSAudioCapture();
|
|
||||||
if (process.platform !== "darwin") {
|
|
||||||
app.quit();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
app.on("before-quit", () => {
|
|
||||||
stopMacOSAudioCapture();
|
|
||||||
});
|
|
||||||
|
|
||||||
app.on("activate", () => {
|
|
||||||
if (BrowserWindow.getAllWindows().length === 0) {
|
|
||||||
createMainWindow();
|
createMainWindow();
|
||||||
}
|
setupGeminiIpcHandlers(geminiSessionRef);
|
||||||
|
setupStorageIpcHandlers();
|
||||||
|
setupGeneralIpcHandlers();
|
||||||
|
});
|
||||||
|
|
||||||
|
app.on('window-all-closed', () => {
|
||||||
|
stopMacOSAudioCapture();
|
||||||
|
if (process.platform !== 'darwin') {
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.on('before-quit', () => {
|
||||||
|
stopMacOSAudioCapture();
|
||||||
|
});
|
||||||
|
|
||||||
|
app.on('activate', () => {
|
||||||
|
if (BrowserWindow.getAllWindows().length === 0) {
|
||||||
|
createMainWindow();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function setupStorageIpcHandlers() {
|
function setupStorageIpcHandlers() {
|
||||||
// ============ CONFIG ============
|
// ============ CONFIG ============
|
||||||
ipcMain.handle("storage:get-config", async () => {
|
ipcMain.handle('storage:get-config', async () => {
|
||||||
try {
|
try {
|
||||||
return { success: true, data: storage.getConfig() };
|
return { success: true, data: storage.getConfig() };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error getting config:", error);
|
console.error('Error getting config:', error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("storage:set-config", async (event, config) => {
|
ipcMain.handle('storage:set-config', async (event, config) => {
|
||||||
try {
|
try {
|
||||||
storage.setConfig(config);
|
storage.setConfig(config);
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error setting config:", error);
|
console.error('Error setting config:', error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("storage:update-config", async (event, key, value) => {
|
ipcMain.handle('storage:update-config', async (event, key, value) => {
|
||||||
try {
|
try {
|
||||||
storage.updateConfig(key, value);
|
storage.updateConfig(key, value);
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error updating config:", error);
|
console.error('Error updating config:', error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ============ CREDENTIALS ============
|
// ============ CREDENTIALS ============
|
||||||
ipcMain.handle("storage:get-credentials", async () => {
|
ipcMain.handle('storage:get-credentials', async () => {
|
||||||
try {
|
try {
|
||||||
return { success: true, data: storage.getCredentials() };
|
return { success: true, data: storage.getCredentials() };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error getting credentials:", error);
|
console.error('Error getting credentials:', error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("storage:set-credentials", async (event, credentials) => {
|
ipcMain.handle('storage:set-credentials', async (event, credentials) => {
|
||||||
try {
|
try {
|
||||||
storage.setCredentials(credentials);
|
storage.setCredentials(credentials);
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error setting credentials:", error);
|
console.error('Error setting credentials:', error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("storage:get-api-key", async () => {
|
ipcMain.handle('storage:get-api-key', async () => {
|
||||||
try {
|
try {
|
||||||
return { success: true, data: storage.getApiKey() };
|
return { success: true, data: storage.getApiKey() };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error getting API key:", error);
|
console.error('Error getting API key:', error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("storage:set-api-key", async (event, apiKey) => {
|
ipcMain.handle('storage:set-api-key', async (event, apiKey) => {
|
||||||
try {
|
try {
|
||||||
storage.setApiKey(apiKey);
|
storage.setApiKey(apiKey);
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error setting API key:", error);
|
console.error('Error setting API key:', error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("storage:get-groq-api-key", async () => {
|
ipcMain.handle('storage:get-groq-api-key', async () => {
|
||||||
try {
|
try {
|
||||||
return { success: true, data: storage.getGroqApiKey() };
|
return { success: true, data: storage.getGroqApiKey() };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error getting Groq API key:", error);
|
console.error('Error getting Groq API key:', error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("storage:set-groq-api-key", async (event, groqApiKey) => {
|
ipcMain.handle('storage:set-groq-api-key', async (event, groqApiKey) => {
|
||||||
try {
|
try {
|
||||||
storage.setGroqApiKey(groqApiKey);
|
storage.setGroqApiKey(groqApiKey);
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error setting Groq API key:", error);
|
console.error('Error setting Groq API key:', error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ============ PREFERENCES ============
|
// ============ PREFERENCES ============
|
||||||
ipcMain.handle("storage:get-preferences", async () => {
|
ipcMain.handle('storage:get-preferences', async () => {
|
||||||
try {
|
try {
|
||||||
return { success: true, data: storage.getPreferences() };
|
return { success: true, data: storage.getPreferences() };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error getting preferences:", error);
|
console.error('Error getting preferences:', error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("storage:set-preferences", async (event, preferences) => {
|
ipcMain.handle('storage:set-preferences', async (event, preferences) => {
|
||||||
try {
|
try {
|
||||||
storage.setPreferences(preferences);
|
storage.setPreferences(preferences);
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error setting preferences:", error);
|
console.error('Error setting preferences:', error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("storage:update-preference", async (event, key, value) => {
|
ipcMain.handle('storage:update-preference', async (event, key, value) => {
|
||||||
try {
|
try {
|
||||||
storage.updatePreference(key, value);
|
storage.updatePreference(key, value);
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error updating preference:", error);
|
console.error('Error updating preference:', error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ============ KEYBINDS ============
|
// ============ KEYBINDS ============
|
||||||
ipcMain.handle("storage:get-keybinds", async () => {
|
ipcMain.handle('storage:get-keybinds', async () => {
|
||||||
try {
|
try {
|
||||||
return { success: true, data: storage.getKeybinds() };
|
return { success: true, data: storage.getKeybinds() };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error getting keybinds:", error);
|
console.error('Error getting keybinds:', error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("storage:set-keybinds", async (event, keybinds) => {
|
ipcMain.handle('storage:set-keybinds', async (event, keybinds) => {
|
||||||
try {
|
try {
|
||||||
storage.setKeybinds(keybinds);
|
storage.setKeybinds(keybinds);
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error setting keybinds:", error);
|
console.error('Error setting keybinds:', error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ============ HISTORY ============
|
// ============ HISTORY ============
|
||||||
ipcMain.handle("storage:get-all-sessions", async () => {
|
ipcMain.handle('storage:get-all-sessions', async () => {
|
||||||
try {
|
try {
|
||||||
return { success: true, data: storage.getAllSessions() };
|
return { success: true, data: storage.getAllSessions() };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error getting sessions:", error);
|
console.error('Error getting sessions:', error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("storage:get-session", async (event, sessionId) => {
|
ipcMain.handle('storage:get-session', async (event, sessionId) => {
|
||||||
try {
|
try {
|
||||||
return { success: true, data: storage.getSession(sessionId) };
|
return { success: true, data: storage.getSession(sessionId) };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error getting session:", error);
|
console.error('Error getting session:', error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("storage:save-session", async (event, sessionId, data) => {
|
ipcMain.handle('storage:save-session', async (event, sessionId, data) => {
|
||||||
try {
|
try {
|
||||||
storage.saveSession(sessionId, data);
|
storage.saveSession(sessionId, data);
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error saving session:", error);
|
console.error('Error saving session:', error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("storage:delete-session", async (event, sessionId) => {
|
ipcMain.handle('storage:delete-session', async (event, sessionId) => {
|
||||||
try {
|
try {
|
||||||
storage.deleteSession(sessionId);
|
storage.deleteSession(sessionId);
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error deleting session:", error);
|
console.error('Error deleting session:', error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("storage:delete-all-sessions", async () => {
|
ipcMain.handle('storage:delete-all-sessions', async () => {
|
||||||
try {
|
try {
|
||||||
storage.deleteAllSessions();
|
storage.deleteAllSessions();
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error deleting all sessions:", error);
|
console.error('Error deleting all sessions:', error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ============ LIMITS ============
|
// ============ LIMITS ============
|
||||||
ipcMain.handle("storage:get-today-limits", async () => {
|
ipcMain.handle('storage:get-today-limits', async () => {
|
||||||
try {
|
try {
|
||||||
return { success: true, data: storage.getTodayLimits() };
|
return { success: true, data: storage.getTodayLimits() };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error getting today limits:", error);
|
console.error('Error getting today limits:', error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ============ CLEAR ALL ============
|
// ============ CLEAR ALL ============
|
||||||
ipcMain.handle("storage:clear-all", async () => {
|
ipcMain.handle('storage:clear-all', async () => {
|
||||||
try {
|
try {
|
||||||
storage.clearAllData();
|
storage.clearAllData();
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error clearing all data:", error);
|
console.error('Error clearing all data:', error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupGeneralIpcHandlers() {
|
function setupGeneralIpcHandlers() {
|
||||||
ipcMain.handle("get-app-version", async () => {
|
ipcMain.handle('get-app-version', async () => {
|
||||||
return app.getVersion();
|
return app.getVersion();
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("quit-application", async (event) => {
|
ipcMain.handle('quit-application', async event => {
|
||||||
try {
|
try {
|
||||||
stopMacOSAudioCapture();
|
stopMacOSAudioCapture();
|
||||||
app.quit();
|
app.quit();
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error quitting application:", error);
|
console.error('Error quitting application:', error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("open-external", async (event, url) => {
|
ipcMain.handle('open-external', async (event, url) => {
|
||||||
try {
|
try {
|
||||||
await shell.openExternal(url);
|
await shell.openExternal(url);
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error opening external URL:", error);
|
console.error('Error opening external URL:', error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.on("update-keybinds", (event, newKeybinds) => {
|
ipcMain.on('update-keybinds', (event, newKeybinds) => {
|
||||||
if (mainWindow) {
|
if (mainWindow) {
|
||||||
// Also save to storage
|
// Also save to storage
|
||||||
storage.setKeybinds(newKeybinds);
|
storage.setKeybinds(newKeybinds);
|
||||||
updateGlobalShortcuts(
|
updateGlobalShortcuts(newKeybinds, mainWindow, sendToRenderer, geminiSessionRef);
|
||||||
newKeybinds,
|
}
|
||||||
mainWindow,
|
});
|
||||||
sendToRenderer,
|
|
||||||
geminiSessionRef,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Debug logging from renderer
|
// Debug logging from renderer
|
||||||
ipcMain.on("log-message", (event, msg) => {
|
ipcMain.on('log-message', (event, msg) => {
|
||||||
console.log(msg);
|
console.log(msg);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
708
src/storage.js
708
src/storage.js
@ -1,574 +1,554 @@
|
|||||||
const fs = require("fs");
|
const fs = require('fs');
|
||||||
const path = require("path");
|
const path = require('path');
|
||||||
const os = require("os");
|
const os = require('os');
|
||||||
|
|
||||||
const CONFIG_VERSION = 1;
|
const CONFIG_VERSION = 1;
|
||||||
|
|
||||||
// Default values
|
// Default values
|
||||||
const DEFAULT_CONFIG = {
|
const DEFAULT_CONFIG = {
|
||||||
configVersion: CONFIG_VERSION,
|
configVersion: CONFIG_VERSION,
|
||||||
onboarded: false,
|
onboarded: false,
|
||||||
layout: "normal",
|
layout: 'normal'
|
||||||
};
|
};
|
||||||
|
|
||||||
const DEFAULT_CREDENTIALS = {
|
const DEFAULT_CREDENTIALS = {
|
||||||
apiKey: "",
|
apiKey: '',
|
||||||
groqApiKey: "",
|
groqApiKey: '',
|
||||||
openaiCompatibleApiKey: "",
|
openaiCompatibleApiKey: '',
|
||||||
openaiCompatibleBaseUrl: "",
|
openaiCompatibleBaseUrl: '',
|
||||||
openaiCompatibleModel: "",
|
openaiCompatibleModel: ''
|
||||||
};
|
};
|
||||||
|
|
||||||
const DEFAULT_PREFERENCES = {
|
const DEFAULT_PREFERENCES = {
|
||||||
customPrompt: "",
|
customPrompt: '',
|
||||||
selectedProfile: "interview",
|
selectedProfile: 'interview',
|
||||||
selectedLanguage: "en-US",
|
selectedLanguage: 'en-US',
|
||||||
selectedScreenshotInterval: "5",
|
selectedScreenshotInterval: '5',
|
||||||
selectedImageQuality: "medium",
|
selectedImageQuality: 'medium',
|
||||||
advancedMode: false,
|
advancedMode: false,
|
||||||
audioMode: "speaker_only",
|
audioMode: 'speaker_only',
|
||||||
fontSize: "medium",
|
fontSize: 'medium',
|
||||||
backgroundTransparency: 0.8,
|
backgroundTransparency: 0.8,
|
||||||
googleSearchEnabled: false,
|
googleSearchEnabled: false,
|
||||||
responseProvider: "gemini",
|
responseProvider: 'gemini',
|
||||||
ollamaHost: "http://127.0.0.1:11434",
|
ollamaHost: 'http://127.0.0.1:11434',
|
||||||
ollamaModel: "llama3.1",
|
ollamaModel: 'llama3.1',
|
||||||
whisperModel: "Xenova/whisper-small",
|
whisperModel: 'Xenova/whisper-small',
|
||||||
whisperDevice: "", // '' = auto-detect, 'cpu' = native, 'wasm' = compatible
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const DEFAULT_KEYBINDS = null; // null means use system defaults
|
const DEFAULT_KEYBINDS = null; // null means use system defaults
|
||||||
|
|
||||||
const DEFAULT_LIMITS = {
|
const DEFAULT_LIMITS = {
|
||||||
data: [], // Array of { date: 'YYYY-MM-DD', flash: { count }, flashLite: { count }, groq: { 'qwen3-32b': { chars, limit }, 'gpt-oss-120b': { chars, limit }, 'gpt-oss-20b': { chars, limit } }, gemini: { 'gemma-3-27b-it': { chars } } }
|
data: [] // Array of { date: 'YYYY-MM-DD', flash: { count }, flashLite: { count }, groq: { 'qwen3-32b': { chars, limit }, 'gpt-oss-120b': { chars, limit }, 'gpt-oss-20b': { chars, limit } }, gemini: { 'gemma-3-27b-it': { chars } } }
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get the config directory path based on OS
|
// Get the config directory path based on OS
|
||||||
function getConfigDir() {
|
function getConfigDir() {
|
||||||
const platform = os.platform();
|
const platform = os.platform();
|
||||||
let configDir;
|
let configDir;
|
||||||
|
|
||||||
if (platform === "win32") {
|
if (platform === 'win32') {
|
||||||
configDir = path.join(
|
configDir = path.join(os.homedir(), 'AppData', 'Roaming', 'cheating-daddy-config');
|
||||||
os.homedir(),
|
} else if (platform === 'darwin') {
|
||||||
"AppData",
|
configDir = path.join(os.homedir(), 'Library', 'Application Support', 'cheating-daddy-config');
|
||||||
"Roaming",
|
} else {
|
||||||
"cheating-daddy-config",
|
configDir = path.join(os.homedir(), '.config', 'cheating-daddy-config');
|
||||||
);
|
}
|
||||||
} else if (platform === "darwin") {
|
|
||||||
configDir = path.join(
|
|
||||||
os.homedir(),
|
|
||||||
"Library",
|
|
||||||
"Application Support",
|
|
||||||
"cheating-daddy-config",
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
configDir = path.join(os.homedir(), ".config", "cheating-daddy-config");
|
|
||||||
}
|
|
||||||
|
|
||||||
return configDir;
|
return configDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
// File paths
|
// File paths
|
||||||
function getConfigPath() {
|
function getConfigPath() {
|
||||||
return path.join(getConfigDir(), "config.json");
|
return path.join(getConfigDir(), 'config.json');
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCredentialsPath() {
|
function getCredentialsPath() {
|
||||||
return path.join(getConfigDir(), "credentials.json");
|
return path.join(getConfigDir(), 'credentials.json');
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPreferencesPath() {
|
function getPreferencesPath() {
|
||||||
return path.join(getConfigDir(), "preferences.json");
|
return path.join(getConfigDir(), 'preferences.json');
|
||||||
}
|
}
|
||||||
|
|
||||||
function getKeybindsPath() {
|
function getKeybindsPath() {
|
||||||
return path.join(getConfigDir(), "keybinds.json");
|
return path.join(getConfigDir(), 'keybinds.json');
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLimitsPath() {
|
function getLimitsPath() {
|
||||||
return path.join(getConfigDir(), "limits.json");
|
return path.join(getConfigDir(), 'limits.json');
|
||||||
}
|
}
|
||||||
|
|
||||||
function getHistoryDir() {
|
function getHistoryDir() {
|
||||||
return path.join(getConfigDir(), "history");
|
return path.join(getConfigDir(), 'history');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper to read JSON file safely
|
// Helper to read JSON file safely
|
||||||
function readJsonFile(filePath, defaultValue) {
|
function readJsonFile(filePath, defaultValue) {
|
||||||
try {
|
try {
|
||||||
if (fs.existsSync(filePath)) {
|
if (fs.existsSync(filePath)) {
|
||||||
const data = fs.readFileSync(filePath, "utf8");
|
const data = fs.readFileSync(filePath, 'utf8');
|
||||||
return JSON.parse(data);
|
return JSON.parse(data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Error reading ${filePath}:`, error.message);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
return defaultValue;
|
||||||
console.warn(`Error reading ${filePath}:`, error.message);
|
|
||||||
}
|
|
||||||
return defaultValue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper to write JSON file safely
|
// Helper to write JSON file safely
|
||||||
function writeJsonFile(filePath, data) {
|
function writeJsonFile(filePath, data) {
|
||||||
try {
|
try {
|
||||||
const dir = path.dirname(filePath);
|
const dir = path.dirname(filePath);
|
||||||
if (!fs.existsSync(dir)) {
|
if (!fs.existsSync(dir)) {
|
||||||
fs.mkdirSync(dir, { recursive: true });
|
fs.mkdirSync(dir, { recursive: true });
|
||||||
|
}
|
||||||
|
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf8');
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error writing ${filePath}:`, error.message);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf8");
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Error writing ${filePath}:`, error.message);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if we need to reset (no configVersion or wrong version)
|
// Check if we need to reset (no configVersion or wrong version)
|
||||||
function needsReset() {
|
function needsReset() {
|
||||||
const configPath = getConfigPath();
|
const configPath = getConfigPath();
|
||||||
if (!fs.existsSync(configPath)) {
|
if (!fs.existsSync(configPath)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
||||||
return !config.configVersion || config.configVersion !== CONFIG_VERSION;
|
return !config.configVersion || config.configVersion !== CONFIG_VERSION;
|
||||||
} catch {
|
} catch {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wipe and reinitialize the config directory
|
// Wipe and reinitialize the config directory
|
||||||
function resetConfigDir() {
|
function resetConfigDir() {
|
||||||
const configDir = getConfigDir();
|
const configDir = getConfigDir();
|
||||||
|
|
||||||
console.log("Resetting config directory...");
|
console.log('Resetting config directory...');
|
||||||
|
|
||||||
// Remove existing directory if it exists
|
// Remove existing directory if it exists
|
||||||
if (fs.existsSync(configDir)) {
|
if (fs.existsSync(configDir)) {
|
||||||
fs.rmSync(configDir, { recursive: true, force: true });
|
fs.rmSync(configDir, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create fresh directory structure
|
// Create fresh directory structure
|
||||||
fs.mkdirSync(configDir, { recursive: true });
|
fs.mkdirSync(configDir, { recursive: true });
|
||||||
fs.mkdirSync(getHistoryDir(), { recursive: true });
|
fs.mkdirSync(getHistoryDir(), { recursive: true });
|
||||||
|
|
||||||
// Initialize with defaults
|
// Initialize with defaults
|
||||||
writeJsonFile(getConfigPath(), DEFAULT_CONFIG);
|
writeJsonFile(getConfigPath(), DEFAULT_CONFIG);
|
||||||
writeJsonFile(getCredentialsPath(), DEFAULT_CREDENTIALS);
|
writeJsonFile(getCredentialsPath(), DEFAULT_CREDENTIALS);
|
||||||
writeJsonFile(getPreferencesPath(), DEFAULT_PREFERENCES);
|
writeJsonFile(getPreferencesPath(), DEFAULT_PREFERENCES);
|
||||||
|
|
||||||
console.log("Config directory initialized with defaults");
|
console.log('Config directory initialized with defaults');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize storage - call this on app startup
|
// Initialize storage - call this on app startup
|
||||||
function initializeStorage() {
|
function initializeStorage() {
|
||||||
if (needsReset()) {
|
if (needsReset()) {
|
||||||
resetConfigDir();
|
resetConfigDir();
|
||||||
} else {
|
} else {
|
||||||
// Ensure history directory exists
|
// Ensure history directory exists
|
||||||
const historyDir = getHistoryDir();
|
const historyDir = getHistoryDir();
|
||||||
if (!fs.existsSync(historyDir)) {
|
if (!fs.existsSync(historyDir)) {
|
||||||
fs.mkdirSync(historyDir, { recursive: true });
|
fs.mkdirSync(historyDir, { recursive: true });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============ CONFIG ============
|
// ============ CONFIG ============
|
||||||
|
|
||||||
function getConfig() {
|
function getConfig() {
|
||||||
return readJsonFile(getConfigPath(), DEFAULT_CONFIG);
|
return readJsonFile(getConfigPath(), DEFAULT_CONFIG);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setConfig(config) {
|
function setConfig(config) {
|
||||||
const current = getConfig();
|
const current = getConfig();
|
||||||
const updated = { ...current, ...config, configVersion: CONFIG_VERSION };
|
const updated = { ...current, ...config, configVersion: CONFIG_VERSION };
|
||||||
return writeJsonFile(getConfigPath(), updated);
|
return writeJsonFile(getConfigPath(), updated);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateConfig(key, value) {
|
function updateConfig(key, value) {
|
||||||
const config = getConfig();
|
const config = getConfig();
|
||||||
config[key] = value;
|
config[key] = value;
|
||||||
return writeJsonFile(getConfigPath(), config);
|
return writeJsonFile(getConfigPath(), config);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============ CREDENTIALS ============
|
// ============ CREDENTIALS ============
|
||||||
|
|
||||||
function getCredentials() {
|
function getCredentials() {
|
||||||
return readJsonFile(getCredentialsPath(), DEFAULT_CREDENTIALS);
|
return readJsonFile(getCredentialsPath(), DEFAULT_CREDENTIALS);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setCredentials(credentials) {
|
function setCredentials(credentials) {
|
||||||
const current = getCredentials();
|
const current = getCredentials();
|
||||||
const updated = { ...current, ...credentials };
|
const updated = { ...current, ...credentials };
|
||||||
return writeJsonFile(getCredentialsPath(), updated);
|
return writeJsonFile(getCredentialsPath(), updated);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getApiKey() {
|
function getApiKey() {
|
||||||
return getCredentials().apiKey || "";
|
return getCredentials().apiKey || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function setApiKey(apiKey) {
|
function setApiKey(apiKey) {
|
||||||
return setCredentials({ apiKey });
|
return setCredentials({ apiKey });
|
||||||
}
|
}
|
||||||
|
|
||||||
function getGroqApiKey() {
|
function getGroqApiKey() {
|
||||||
return getCredentials().groqApiKey || "";
|
return getCredentials().groqApiKey || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function setGroqApiKey(groqApiKey) {
|
function setGroqApiKey(groqApiKey) {
|
||||||
return setCredentials({ groqApiKey });
|
return setCredentials({ groqApiKey });
|
||||||
}
|
}
|
||||||
|
|
||||||
function getOpenAICompatibleConfig() {
|
function getOpenAICompatibleConfig() {
|
||||||
const creds = getCredentials();
|
const creds = getCredentials();
|
||||||
return {
|
return {
|
||||||
apiKey: creds.openaiCompatibleApiKey || "",
|
apiKey: creds.openaiCompatibleApiKey || '',
|
||||||
baseUrl: creds.openaiCompatibleBaseUrl || "",
|
baseUrl: creds.openaiCompatibleBaseUrl || '',
|
||||||
model: creds.openaiCompatibleModel || "",
|
model: creds.openaiCompatibleModel || ''
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function setOpenAICompatibleConfig(apiKey, baseUrl, model) {
|
function setOpenAICompatibleConfig(apiKey, baseUrl, model) {
|
||||||
return setCredentials({
|
return setCredentials({
|
||||||
openaiCompatibleApiKey: apiKey,
|
openaiCompatibleApiKey: apiKey,
|
||||||
openaiCompatibleBaseUrl: baseUrl,
|
openaiCompatibleBaseUrl: baseUrl,
|
||||||
openaiCompatibleModel: model,
|
openaiCompatibleModel: model
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============ PREFERENCES ============
|
// ============ PREFERENCES ============
|
||||||
|
|
||||||
function getPreferences() {
|
function getPreferences() {
|
||||||
const saved = readJsonFile(getPreferencesPath(), {});
|
const saved = readJsonFile(getPreferencesPath(), {});
|
||||||
return { ...DEFAULT_PREFERENCES, ...saved };
|
return { ...DEFAULT_PREFERENCES, ...saved };
|
||||||
}
|
}
|
||||||
|
|
||||||
function setPreferences(preferences) {
|
function setPreferences(preferences) {
|
||||||
const current = getPreferences();
|
const current = getPreferences();
|
||||||
const updated = { ...current, ...preferences };
|
const updated = { ...current, ...preferences };
|
||||||
return writeJsonFile(getPreferencesPath(), updated);
|
return writeJsonFile(getPreferencesPath(), updated);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updatePreference(key, value) {
|
function updatePreference(key, value) {
|
||||||
const preferences = getPreferences();
|
const preferences = getPreferences();
|
||||||
preferences[key] = value;
|
preferences[key] = value;
|
||||||
return writeJsonFile(getPreferencesPath(), preferences);
|
return writeJsonFile(getPreferencesPath(), preferences);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============ KEYBINDS ============
|
// ============ KEYBINDS ============
|
||||||
|
|
||||||
function getKeybinds() {
|
function getKeybinds() {
|
||||||
return readJsonFile(getKeybindsPath(), DEFAULT_KEYBINDS);
|
return readJsonFile(getKeybindsPath(), DEFAULT_KEYBINDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setKeybinds(keybinds) {
|
function setKeybinds(keybinds) {
|
||||||
return writeJsonFile(getKeybindsPath(), keybinds);
|
return writeJsonFile(getKeybindsPath(), keybinds);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============ LIMITS (Rate Limiting) ============
|
// ============ LIMITS (Rate Limiting) ============
|
||||||
|
|
||||||
function getLimits() {
|
function getLimits() {
|
||||||
return readJsonFile(getLimitsPath(), DEFAULT_LIMITS);
|
return readJsonFile(getLimitsPath(), DEFAULT_LIMITS);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setLimits(limits) {
|
function setLimits(limits) {
|
||||||
return writeJsonFile(getLimitsPath(), limits);
|
return writeJsonFile(getLimitsPath(), limits);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTodayDateString() {
|
function getTodayDateString() {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
return now.toISOString().split("T")[0]; // YYYY-MM-DD
|
return now.toISOString().split('T')[0]; // YYYY-MM-DD
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTodayLimits() {
|
function getTodayLimits() {
|
||||||
const limits = getLimits();
|
const limits = getLimits();
|
||||||
const today = getTodayDateString();
|
const today = getTodayDateString();
|
||||||
|
|
||||||
// Find today's entry
|
// Find today's entry
|
||||||
const todayEntry = limits.data.find((entry) => entry.date === today);
|
const todayEntry = limits.data.find(entry => entry.date === today);
|
||||||
|
|
||||||
if (todayEntry) {
|
if (todayEntry) {
|
||||||
// ensure new fields exist
|
// ensure new fields exist
|
||||||
if (!todayEntry.groq) {
|
if(!todayEntry.groq) {
|
||||||
todayEntry.groq = {
|
todayEntry.groq = {
|
||||||
"qwen3-32b": { chars: 0, limit: 1500000 },
|
'qwen3-32b': { chars: 0, limit: 1500000 },
|
||||||
"gpt-oss-120b": { chars: 0, limit: 600000 },
|
'gpt-oss-120b': { chars: 0, limit: 600000 },
|
||||||
"gpt-oss-20b": { chars: 0, limit: 600000 },
|
'gpt-oss-20b': { chars: 0, limit: 600000 },
|
||||||
"kimi-k2-instruct": { chars: 0, limit: 600000 },
|
'kimi-k2-instruct': { chars: 0, limit: 600000 }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (!todayEntry.gemini) {
|
if(!todayEntry.gemini) {
|
||||||
todayEntry.gemini = {
|
todayEntry.gemini = {
|
||||||
"gemma-3-27b-it": { chars: 0 },
|
'gemma-3-27b-it': { chars: 0 }
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
setLimits(limits);
|
||||||
|
return todayEntry;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// No entry for today - clean old entries and create new one
|
||||||
|
limits.data = limits.data.filter(entry => entry.date === today);
|
||||||
|
const newEntry = {
|
||||||
|
date: today,
|
||||||
|
flash: { count: 0 },
|
||||||
|
flashLite: { count: 0 },
|
||||||
|
groq: {
|
||||||
|
'qwen3-32b': { chars: 0, limit: 1500000 },
|
||||||
|
'gpt-oss-120b': { chars: 0, limit: 600000 },
|
||||||
|
'gpt-oss-20b': { chars: 0, limit: 600000 },
|
||||||
|
'kimi-k2-instruct': { chars: 0, limit: 600000 }
|
||||||
|
},
|
||||||
|
gemini: {
|
||||||
|
'gemma-3-27b-it': { chars: 0 }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
limits.data.push(newEntry);
|
||||||
setLimits(limits);
|
setLimits(limits);
|
||||||
return todayEntry;
|
|
||||||
}
|
|
||||||
|
|
||||||
// No entry for today - clean old entries and create new one
|
return newEntry;
|
||||||
limits.data = limits.data.filter((entry) => entry.date === today);
|
|
||||||
const newEntry = {
|
|
||||||
date: today,
|
|
||||||
flash: { count: 0 },
|
|
||||||
flashLite: { count: 0 },
|
|
||||||
groq: {
|
|
||||||
"qwen3-32b": { chars: 0, limit: 1500000 },
|
|
||||||
"gpt-oss-120b": { chars: 0, limit: 600000 },
|
|
||||||
"gpt-oss-20b": { chars: 0, limit: 600000 },
|
|
||||||
"kimi-k2-instruct": { chars: 0, limit: 600000 },
|
|
||||||
},
|
|
||||||
gemini: {
|
|
||||||
"gemma-3-27b-it": { chars: 0 },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
limits.data.push(newEntry);
|
|
||||||
setLimits(limits);
|
|
||||||
|
|
||||||
return newEntry;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function incrementLimitCount(model) {
|
function incrementLimitCount(model) {
|
||||||
const limits = getLimits();
|
const limits = getLimits();
|
||||||
const today = getTodayDateString();
|
const today = getTodayDateString();
|
||||||
|
|
||||||
// Find or create today's entry
|
// Find or create today's entry
|
||||||
let todayEntry = limits.data.find((entry) => entry.date === today);
|
let todayEntry = limits.data.find(entry => entry.date === today);
|
||||||
|
|
||||||
if (!todayEntry) {
|
if (!todayEntry) {
|
||||||
// Clean old entries and create new one
|
// Clean old entries and create new one
|
||||||
limits.data = [];
|
limits.data = [];
|
||||||
todayEntry = {
|
todayEntry = {
|
||||||
date: today,
|
date: today,
|
||||||
flash: { count: 0 },
|
flash: { count: 0 },
|
||||||
flashLite: { count: 0 },
|
flashLite: { count: 0 }
|
||||||
};
|
};
|
||||||
limits.data.push(todayEntry);
|
limits.data.push(todayEntry);
|
||||||
} else {
|
} else {
|
||||||
// Clean old entries, keep only today
|
// Clean old entries, keep only today
|
||||||
limits.data = limits.data.filter((entry) => entry.date === today);
|
limits.data = limits.data.filter(entry => entry.date === today);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Increment the appropriate model count
|
// Increment the appropriate model count
|
||||||
if (model === "gemini-2.5-flash") {
|
if (model === 'gemini-2.5-flash') {
|
||||||
todayEntry.flash.count++;
|
todayEntry.flash.count++;
|
||||||
} else if (model === "gemini-2.5-flash-lite") {
|
} else if (model === 'gemini-2.5-flash-lite') {
|
||||||
todayEntry.flashLite.count++;
|
todayEntry.flashLite.count++;
|
||||||
}
|
}
|
||||||
|
|
||||||
setLimits(limits);
|
setLimits(limits);
|
||||||
return todayEntry;
|
return todayEntry;
|
||||||
}
|
}
|
||||||
|
|
||||||
function incrementCharUsage(provider, model, charCount) {
|
function incrementCharUsage(provider, model, charCount) {
|
||||||
getTodayLimits();
|
getTodayLimits();
|
||||||
|
|
||||||
const limits = getLimits();
|
const limits = getLimits();
|
||||||
const today = getTodayDateString();
|
const today = getTodayDateString();
|
||||||
const todayEntry = limits.data.find((entry) => entry.date === today);
|
const todayEntry = limits.data.find(entry => entry.date === today);
|
||||||
|
|
||||||
if (todayEntry[provider] && todayEntry[provider][model]) {
|
if(todayEntry[provider] && todayEntry[provider][model]) {
|
||||||
todayEntry[provider][model].chars += charCount;
|
todayEntry[provider][model].chars += charCount;
|
||||||
setLimits(limits);
|
setLimits(limits);
|
||||||
}
|
}
|
||||||
|
|
||||||
return todayEntry;
|
return todayEntry;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAvailableModel() {
|
function getAvailableModel() {
|
||||||
const todayLimits = getTodayLimits();
|
const todayLimits = getTodayLimits();
|
||||||
|
|
||||||
// RPD limits: flash = 20, flash-lite = 20
|
// RPD limits: flash = 20, flash-lite = 20
|
||||||
// After both exhausted, fall back to flash (for paid API users)
|
// After both exhausted, fall back to flash (for paid API users)
|
||||||
if (todayLimits.flash.count < 20) {
|
if (todayLimits.flash.count < 20) {
|
||||||
return "gemini-2.5-flash";
|
return 'gemini-2.5-flash';
|
||||||
} else if (todayLimits.flashLite.count < 20) {
|
} else if (todayLimits.flashLite.count < 20) {
|
||||||
return "gemini-2.5-flash-lite";
|
return 'gemini-2.5-flash-lite';
|
||||||
}
|
}
|
||||||
|
|
||||||
return "gemini-2.5-flash"; // Default to flash for paid API users
|
return 'gemini-2.5-flash'; // Default to flash for paid API users
|
||||||
}
|
}
|
||||||
|
|
||||||
function getModelForToday() {
|
function getModelForToday() {
|
||||||
const todayEntry = getTodayLimits();
|
const todayEntry = getTodayLimits();
|
||||||
const groq = todayEntry.groq;
|
const groq = todayEntry.groq;
|
||||||
|
|
||||||
if (groq["qwen3-32b"].chars < groq["qwen3-32b"].limit) {
|
if (groq['qwen3-32b'].chars < groq['qwen3-32b'].limit) {
|
||||||
return "qwen/qwen3-32b";
|
return 'qwen/qwen3-32b';
|
||||||
}
|
}
|
||||||
if (groq["gpt-oss-120b"].chars < groq["gpt-oss-120b"].limit) {
|
if (groq['gpt-oss-120b'].chars < groq['gpt-oss-120b'].limit) {
|
||||||
return "openai/gpt-oss-120b";
|
return 'openai/gpt-oss-120b';
|
||||||
}
|
}
|
||||||
if (groq["gpt-oss-20b"].chars < groq["gpt-oss-20b"].limit) {
|
if (groq['gpt-oss-20b'].chars < groq['gpt-oss-20b'].limit) {
|
||||||
return "openai/gpt-oss-20b";
|
return 'openai/gpt-oss-20b';
|
||||||
}
|
}
|
||||||
if (groq["kimi-k2-instruct"].chars < groq["kimi-k2-instruct"].limit) {
|
if (groq['kimi-k2-instruct'].chars < groq['kimi-k2-instruct'].limit) {
|
||||||
return "moonshotai/kimi-k2-instruct";
|
return 'moonshotai/kimi-k2-instruct';
|
||||||
}
|
}
|
||||||
|
|
||||||
// All limits exhausted
|
// All limits exhausted
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============ HISTORY ============
|
// ============ HISTORY ============
|
||||||
|
|
||||||
function getSessionPath(sessionId) {
|
function getSessionPath(sessionId) {
|
||||||
return path.join(getHistoryDir(), `${sessionId}.json`);
|
return path.join(getHistoryDir(), `${sessionId}.json`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveSession(sessionId, data) {
|
function saveSession(sessionId, data) {
|
||||||
const sessionPath = getSessionPath(sessionId);
|
const sessionPath = getSessionPath(sessionId);
|
||||||
|
|
||||||
// Load existing session to preserve metadata
|
// Load existing session to preserve metadata
|
||||||
const existingSession = readJsonFile(sessionPath, null);
|
const existingSession = readJsonFile(sessionPath, null);
|
||||||
|
|
||||||
const sessionData = {
|
const sessionData = {
|
||||||
sessionId,
|
sessionId,
|
||||||
createdAt: existingSession?.createdAt || parseInt(sessionId),
|
createdAt: existingSession?.createdAt || parseInt(sessionId),
|
||||||
lastUpdated: Date.now(),
|
lastUpdated: Date.now(),
|
||||||
// Profile context - set once when session starts
|
// Profile context - set once when session starts
|
||||||
profile: data.profile || existingSession?.profile || null,
|
profile: data.profile || existingSession?.profile || null,
|
||||||
customPrompt: data.customPrompt || existingSession?.customPrompt || null,
|
customPrompt: data.customPrompt || existingSession?.customPrompt || null,
|
||||||
// Conversation data
|
// Conversation data
|
||||||
conversationHistory:
|
conversationHistory: data.conversationHistory || existingSession?.conversationHistory || [],
|
||||||
data.conversationHistory || existingSession?.conversationHistory || [],
|
screenAnalysisHistory: data.screenAnalysisHistory || existingSession?.screenAnalysisHistory || []
|
||||||
screenAnalysisHistory:
|
};
|
||||||
data.screenAnalysisHistory ||
|
return writeJsonFile(sessionPath, sessionData);
|
||||||
existingSession?.screenAnalysisHistory ||
|
|
||||||
[],
|
|
||||||
};
|
|
||||||
return writeJsonFile(sessionPath, sessionData);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSession(sessionId) {
|
function getSession(sessionId) {
|
||||||
return readJsonFile(getSessionPath(sessionId), null);
|
return readJsonFile(getSessionPath(sessionId), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAllSessions() {
|
function getAllSessions() {
|
||||||
const historyDir = getHistoryDir();
|
const historyDir = getHistoryDir();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!fs.existsSync(historyDir)) {
|
if (!fs.existsSync(historyDir)) {
|
||||||
return [];
|
return [];
|
||||||
}
|
|
||||||
|
|
||||||
const files = fs
|
|
||||||
.readdirSync(historyDir)
|
|
||||||
.filter((f) => f.endsWith(".json"))
|
|
||||||
.sort((a, b) => {
|
|
||||||
// Sort by timestamp descending (newest first)
|
|
||||||
const tsA = parseInt(a.replace(".json", ""));
|
|
||||||
const tsB = parseInt(b.replace(".json", ""));
|
|
||||||
return tsB - tsA;
|
|
||||||
});
|
|
||||||
|
|
||||||
return files
|
|
||||||
.map((file) => {
|
|
||||||
const sessionId = file.replace(".json", "");
|
|
||||||
const data = readJsonFile(path.join(historyDir, file), null);
|
|
||||||
if (data) {
|
|
||||||
return {
|
|
||||||
sessionId,
|
|
||||||
createdAt: data.createdAt,
|
|
||||||
lastUpdated: data.lastUpdated,
|
|
||||||
messageCount: data.conversationHistory?.length || 0,
|
|
||||||
screenAnalysisCount: data.screenAnalysisHistory?.length || 0,
|
|
||||||
profile: data.profile || null,
|
|
||||||
customPrompt: data.customPrompt || null,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
})
|
const files = fs.readdirSync(historyDir)
|
||||||
.filter(Boolean);
|
.filter(f => f.endsWith('.json'))
|
||||||
} catch (error) {
|
.sort((a, b) => {
|
||||||
console.error("Error reading sessions:", error.message);
|
// Sort by timestamp descending (newest first)
|
||||||
return [];
|
const tsA = parseInt(a.replace('.json', ''));
|
||||||
}
|
const tsB = parseInt(b.replace('.json', ''));
|
||||||
|
return tsB - tsA;
|
||||||
|
});
|
||||||
|
|
||||||
|
return files.map(file => {
|
||||||
|
const sessionId = file.replace('.json', '');
|
||||||
|
const data = readJsonFile(path.join(historyDir, file), null);
|
||||||
|
if (data) {
|
||||||
|
return {
|
||||||
|
sessionId,
|
||||||
|
createdAt: data.createdAt,
|
||||||
|
lastUpdated: data.lastUpdated,
|
||||||
|
messageCount: data.conversationHistory?.length || 0,
|
||||||
|
screenAnalysisCount: data.screenAnalysisHistory?.length || 0,
|
||||||
|
profile: data.profile || null,
|
||||||
|
customPrompt: data.customPrompt || null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}).filter(Boolean);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error reading sessions:', error.message);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteSession(sessionId) {
|
function deleteSession(sessionId) {
|
||||||
const sessionPath = getSessionPath(sessionId);
|
const sessionPath = getSessionPath(sessionId);
|
||||||
try {
|
try {
|
||||||
if (fs.existsSync(sessionPath)) {
|
if (fs.existsSync(sessionPath)) {
|
||||||
fs.unlinkSync(sessionPath);
|
fs.unlinkSync(sessionPath);
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting session:', error.message);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
return false;
|
||||||
console.error("Error deleting session:", error.message);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteAllSessions() {
|
function deleteAllSessions() {
|
||||||
const historyDir = getHistoryDir();
|
const historyDir = getHistoryDir();
|
||||||
try {
|
try {
|
||||||
if (fs.existsSync(historyDir)) {
|
if (fs.existsSync(historyDir)) {
|
||||||
const files = fs
|
const files = fs.readdirSync(historyDir).filter(f => f.endsWith('.json'));
|
||||||
.readdirSync(historyDir)
|
files.forEach(file => {
|
||||||
.filter((f) => f.endsWith(".json"));
|
fs.unlinkSync(path.join(historyDir, file));
|
||||||
files.forEach((file) => {
|
});
|
||||||
fs.unlinkSync(path.join(historyDir, file));
|
}
|
||||||
});
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting all sessions:', error.message);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error deleting all sessions:", error.message);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============ CLEAR ALL DATA ============
|
// ============ CLEAR ALL DATA ============
|
||||||
|
|
||||||
function clearAllData() {
|
function clearAllData() {
|
||||||
resetConfigDir();
|
resetConfigDir();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
// Initialization
|
// Initialization
|
||||||
initializeStorage,
|
initializeStorage,
|
||||||
getConfigDir,
|
getConfigDir,
|
||||||
|
|
||||||
// Config
|
// Config
|
||||||
getConfig,
|
getConfig,
|
||||||
setConfig,
|
setConfig,
|
||||||
updateConfig,
|
updateConfig,
|
||||||
|
|
||||||
// Credentials
|
// Credentials
|
||||||
getCredentials,
|
getCredentials,
|
||||||
setCredentials,
|
setCredentials,
|
||||||
getApiKey,
|
getApiKey,
|
||||||
setApiKey,
|
setApiKey,
|
||||||
getGroqApiKey,
|
getGroqApiKey,
|
||||||
setGroqApiKey,
|
setGroqApiKey,
|
||||||
getOpenAICompatibleConfig,
|
getOpenAICompatibleConfig,
|
||||||
setOpenAICompatibleConfig,
|
setOpenAICompatibleConfig,
|
||||||
|
|
||||||
// Preferences
|
// Preferences
|
||||||
getPreferences,
|
getPreferences,
|
||||||
setPreferences,
|
setPreferences,
|
||||||
updatePreference,
|
updatePreference,
|
||||||
|
|
||||||
// Keybinds
|
// Keybinds
|
||||||
getKeybinds,
|
getKeybinds,
|
||||||
setKeybinds,
|
setKeybinds,
|
||||||
|
|
||||||
// Limits (Rate Limiting)
|
// Limits (Rate Limiting)
|
||||||
getLimits,
|
getLimits,
|
||||||
setLimits,
|
setLimits,
|
||||||
getTodayLimits,
|
getTodayLimits,
|
||||||
incrementLimitCount,
|
incrementLimitCount,
|
||||||
getAvailableModel,
|
getAvailableModel,
|
||||||
incrementCharUsage,
|
incrementCharUsage,
|
||||||
getModelForToday,
|
getModelForToday,
|
||||||
|
|
||||||
// History
|
// History
|
||||||
saveSession,
|
saveSession,
|
||||||
getSession,
|
getSession,
|
||||||
getAllSessions,
|
getAllSessions,
|
||||||
deleteSession,
|
deleteSession,
|
||||||
deleteAllSessions,
|
deleteAllSessions,
|
||||||
|
|
||||||
// Clear all
|
// Clear all
|
||||||
clearAllData,
|
clearAllData
|
||||||
};
|
};
|
||||||
|
|||||||
1051
src/utils/localai.js
1051
src/utils/localai.js
File diff suppressed because it is too large
Load Diff
@ -1,177 +0,0 @@
|
|||||||
/**
|
|
||||||
* nodeDetect.js — Locate the system Node.js binary.
|
|
||||||
*
|
|
||||||
* When spawning child processes that rely on native addons compiled against the
|
|
||||||
* system Node.js ABI (e.g. onnxruntime-node), we must NOT run them inside
|
|
||||||
* Electron's embedded Node.js runtime — the ABI mismatch causes SIGTRAP /
|
|
||||||
* SIGSEGV crashes. This module finds the real system `node` binary so we can
|
|
||||||
* pass it as `execPath` to `child_process.fork()`.
|
|
||||||
*
|
|
||||||
* Falls back to `null` when no system Node.js is found, letting the caller
|
|
||||||
* decide on an alternative strategy (e.g. WASM backend).
|
|
||||||
*/
|
|
||||||
|
|
||||||
const { execSync } = require("child_process");
|
|
||||||
const fs = require("fs");
|
|
||||||
const path = require("path");
|
|
||||||
const os = require("os");
|
|
||||||
|
|
||||||
/** Well-known Node.js install locations per platform. */
|
|
||||||
const KNOWN_PATHS = {
|
|
||||||
darwin: [
|
|
||||||
"/usr/local/bin/node",
|
|
||||||
"/opt/homebrew/bin/node", // Apple Silicon Homebrew
|
|
||||||
path.join(os.homedir(), ".nvm/versions/node"), // nvm — needs glob
|
|
||||||
path.join(os.homedir(), ".volta/bin/node"), // Volta
|
|
||||||
path.join(os.homedir(), ".fnm/aliases/default/bin/node"), // fnm
|
|
||||||
path.join(os.homedir(), ".mise/shims/node"), // mise (rtx)
|
|
||||||
path.join(os.homedir(), ".asdf/shims/node"), // asdf
|
|
||||||
],
|
|
||||||
linux: [
|
|
||||||
"/usr/bin/node",
|
|
||||||
"/usr/local/bin/node",
|
|
||||||
path.join(os.homedir(), ".nvm/versions/node"),
|
|
||||||
path.join(os.homedir(), ".volta/bin/node"),
|
|
||||||
path.join(os.homedir(), ".fnm/aliases/default/bin/node"),
|
|
||||||
path.join(os.homedir(), ".mise/shims/node"),
|
|
||||||
path.join(os.homedir(), ".asdf/shims/node"),
|
|
||||||
],
|
|
||||||
win32: [
|
|
||||||
"C:\\Program Files\\nodejs\\node.exe",
|
|
||||||
"C:\\Program Files (x86)\\nodejs\\node.exe",
|
|
||||||
path.join(os.homedir(), "AppData", "Roaming", "nvm", "current", "node.exe"),
|
|
||||||
path.join(os.homedir(), ".volta", "bin", "node.exe"),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find the latest nvm-installed Node.js binary on macOS / Linux.
|
|
||||||
* Returns the path to the `node` binary or null.
|
|
||||||
*/
|
|
||||||
function findNvmNode() {
|
|
||||||
const nvmDir = path.join(os.homedir(), ".nvm", "versions", "node");
|
|
||||||
try {
|
|
||||||
if (!fs.existsSync(nvmDir)) return null;
|
|
||||||
const versions = fs.readdirSync(nvmDir).filter((d) => d.startsWith("v"));
|
|
||||||
if (versions.length === 0) return null;
|
|
||||||
// Sort semver descending (rough but sufficient)
|
|
||||||
versions.sort((a, b) => b.localeCompare(a, undefined, { numeric: true }));
|
|
||||||
const nodeBin = path.join(nvmDir, versions[0], "bin", "node");
|
|
||||||
if (fs.existsSync(nodeBin)) return nodeBin;
|
|
||||||
} catch (_) {
|
|
||||||
// Ignore
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attempt to resolve `node` via the system PATH using `which` (Unix) or
|
|
||||||
* `where` (Windows). Returns the path string or null.
|
|
||||||
*/
|
|
||||||
function whichNode() {
|
|
||||||
try {
|
|
||||||
const cmd = process.platform === "win32" ? "where node" : "which node";
|
|
||||||
const result = execSync(cmd, {
|
|
||||||
encoding: "utf8",
|
|
||||||
timeout: 5000,
|
|
||||||
env: {
|
|
||||||
...process.env,
|
|
||||||
// Ensure common manager shim dirs are on PATH
|
|
||||||
PATH: [
|
|
||||||
process.env.PATH || "",
|
|
||||||
"/usr/local/bin",
|
|
||||||
"/opt/homebrew/bin",
|
|
||||||
path.join(os.homedir(), ".volta", "bin"),
|
|
||||||
path.join(os.homedir(), ".fnm", "aliases", "default", "bin"),
|
|
||||||
path.join(os.homedir(), ".mise", "shims"),
|
|
||||||
path.join(os.homedir(), ".asdf", "shims"),
|
|
||||||
].join(process.platform === "win32" ? ";" : ":"),
|
|
||||||
},
|
|
||||||
stdio: ["ignore", "pipe", "ignore"],
|
|
||||||
}).trim();
|
|
||||||
// `where` on Windows may return multiple lines — take the first
|
|
||||||
const first = result.split(/\r?\n/)[0].trim();
|
|
||||||
if (first && fs.existsSync(first)) return first;
|
|
||||||
} catch (_) {
|
|
||||||
// Command failed
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check whether a given path is a real Node.js binary (not the Electron binary
|
|
||||||
* pretending to be Node via ELECTRON_RUN_AS_NODE).
|
|
||||||
*/
|
|
||||||
function isRealNode(nodePath) {
|
|
||||||
if (!nodePath) return false;
|
|
||||||
try {
|
|
||||||
const out = execSync(
|
|
||||||
`"${nodePath}" -e "process.stdout.write(String(!process.versions.electron))"`,
|
|
||||||
{
|
|
||||||
encoding: "utf8",
|
|
||||||
timeout: 5000,
|
|
||||||
env: { ...process.env, ELECTRON_RUN_AS_NODE: undefined },
|
|
||||||
stdio: ["ignore", "pipe", "ignore"],
|
|
||||||
},
|
|
||||||
).trim();
|
|
||||||
return out === "true";
|
|
||||||
} catch (_) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find the system Node.js binary.
|
|
||||||
*
|
|
||||||
* @returns {{ nodePath: string } | null} The absolute path to system `node`,
|
|
||||||
* or null if none found. The caller should fall back to WASM when null.
|
|
||||||
*/
|
|
||||||
function findSystemNode() {
|
|
||||||
// 1. Try `which node` / `where node` first (respects user's PATH / shims)
|
|
||||||
const fromPath = whichNode();
|
|
||||||
if (fromPath && isRealNode(fromPath)) {
|
|
||||||
return { nodePath: fromPath };
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Try nvm (has multiple version dirs)
|
|
||||||
const fromNvm = findNvmNode();
|
|
||||||
if (fromNvm && isRealNode(fromNvm)) {
|
|
||||||
return { nodePath: fromNvm };
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Walk the well-known paths for the current platform
|
|
||||||
const platform = process.platform;
|
|
||||||
const candidates = KNOWN_PATHS[platform] || KNOWN_PATHS.linux;
|
|
||||||
for (const candidate of candidates) {
|
|
||||||
// Skip the nvm root — already handled above
|
|
||||||
if (candidate.includes(".nvm/versions/node")) continue;
|
|
||||||
if (fs.existsSync(candidate) && isRealNode(candidate)) {
|
|
||||||
return { nodePath: candidate };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Cache so we only search once per process lifetime. */
|
|
||||||
let _cached = undefined;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cached version of `findSystemNode()`.
|
|
||||||
* @returns {{ nodePath: string } | null}
|
|
||||||
*/
|
|
||||||
function getSystemNode() {
|
|
||||||
if (_cached === undefined) {
|
|
||||||
_cached = findSystemNode();
|
|
||||||
if (_cached) {
|
|
||||||
console.log("[nodeDetect] Found system Node.js:", _cached.nodePath);
|
|
||||||
} else {
|
|
||||||
console.warn(
|
|
||||||
"[nodeDetect] No system Node.js found — will fall back to WASM backend",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return _cached;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { findSystemNode, getSystemNode, isRealNode };
|
|
||||||
@ -1,332 +0,0 @@
|
|||||||
/**
|
|
||||||
* Whisper Worker — runs ONNX Runtime in an isolated child process.
|
|
||||||
*
|
|
||||||
* The main Electron process forks this file and communicates via IPC messages.
|
|
||||||
* If ONNX Runtime crashes (SIGSEGV/SIGABRT inside the native Metal or CPU
|
|
||||||
* execution provider), only this worker dies — the main process survives and
|
|
||||||
* can respawn the worker automatically.
|
|
||||||
*
|
|
||||||
* Protocol (parent ↔ worker):
|
|
||||||
* parent → worker:
|
|
||||||
* { type: 'load', modelName, cacheDir, device? }
|
|
||||||
* { type: 'transcribe', audioBase64, language? } // PCM 16-bit 16kHz as base64
|
|
||||||
* { type: 'shutdown' }
|
|
||||||
*
|
|
||||||
* worker → parent:
|
|
||||||
* { type: 'load-result', success, error?, device? }
|
|
||||||
* { type: 'transcribe-result', success, text?, error? }
|
|
||||||
* { type: 'status', message }
|
|
||||||
* { type: 'ready' }
|
|
||||||
*/
|
|
||||||
|
|
||||||
// ── Crash handlers — report fatal errors before the process dies ──
|
|
||||||
|
|
||||||
process.on("uncaughtException", (err) => {
|
|
||||||
try {
|
|
||||||
send({
|
|
||||||
type: "status",
|
|
||||||
message: `[Worker] Uncaught exception: ${err.message || err}`,
|
|
||||||
});
|
|
||||||
console.error("[WhisperWorker] Uncaught exception:", err);
|
|
||||||
} catch (_) {
|
|
||||||
// Cannot communicate with parent anymore
|
|
||||||
}
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
process.on("unhandledRejection", (reason) => {
|
|
||||||
try {
|
|
||||||
send({
|
|
||||||
type: "status",
|
|
||||||
message: `[Worker] Unhandled rejection: ${reason?.message || reason}`,
|
|
||||||
});
|
|
||||||
console.error("[WhisperWorker] Unhandled rejection:", reason);
|
|
||||||
} catch (_) {
|
|
||||||
// Cannot communicate with parent anymore
|
|
||||||
}
|
|
||||||
// Don't exit — let it be caught by the pipeline's own handlers
|
|
||||||
});
|
|
||||||
|
|
||||||
let whisperPipeline = null;
|
|
||||||
/** Which ONNX backend is actually active: "cpu" | "wasm" */
|
|
||||||
let activeDevice = null;
|
|
||||||
|
|
||||||
function pcm16ToFloat32(pcm16Buffer) {
|
|
||||||
if (!pcm16Buffer || pcm16Buffer.length === 0) {
|
|
||||||
return new Float32Array(0);
|
|
||||||
}
|
|
||||||
const alignedLength =
|
|
||||||
pcm16Buffer.length % 2 === 0 ? pcm16Buffer.length : pcm16Buffer.length - 1;
|
|
||||||
const samples = alignedLength / 2;
|
|
||||||
const float32 = new Float32Array(samples);
|
|
||||||
for (let i = 0; i < samples; i++) {
|
|
||||||
float32[i] = pcm16Buffer.readInt16LE(i * 2) / 32768;
|
|
||||||
}
|
|
||||||
return float32;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load the Whisper model.
|
|
||||||
*
|
|
||||||
* @param {string} modelName HuggingFace model id, e.g. "Xenova/whisper-small"
|
|
||||||
* @param {string} cacheDir Directory for cached model files
|
|
||||||
* @param {string} [device] "cpu" (onnxruntime-node) or "wasm" (onnxruntime-web).
|
|
||||||
* When "cpu" is requested we try native first and fall
|
|
||||||
* back to "wasm" on failure (ABI mismatch, etc.).
|
|
||||||
*/
|
|
||||||
async function loadModel(modelName, cacheDir, device = "cpu") {
|
|
||||||
if (whisperPipeline) {
|
|
||||||
send({ type: "load-result", success: true, device: activeDevice });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
send({
|
|
||||||
type: "status",
|
|
||||||
message: "Loading Whisper model (first time may take a while)...",
|
|
||||||
});
|
|
||||||
|
|
||||||
// Validate / create cache directory
|
|
||||||
const fs = require("fs");
|
|
||||||
const path = require("path");
|
|
||||||
if (cacheDir) {
|
|
||||||
try {
|
|
||||||
if (!fs.existsSync(cacheDir)) {
|
|
||||||
fs.mkdirSync(cacheDir, { recursive: true });
|
|
||||||
console.log("[WhisperWorker] Created cache directory:", cacheDir);
|
|
||||||
}
|
|
||||||
} catch (mkdirErr) {
|
|
||||||
console.warn(
|
|
||||||
"[WhisperWorker] Cannot create cache dir:",
|
|
||||||
mkdirErr.message,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for corrupted partial downloads — if an onnx file exists but
|
|
||||||
// is suspiciously small (< 1 KB), delete it so the library re-downloads.
|
|
||||||
try {
|
|
||||||
const modelDir = path.join(cacheDir, modelName.replace("/", path.sep));
|
|
||||||
if (fs.existsSync(modelDir)) {
|
|
||||||
const walk = (dir) => {
|
|
||||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
||||||
const full = path.join(dir, entry.name);
|
|
||||||
if (entry.isDirectory()) {
|
|
||||||
walk(full);
|
|
||||||
} else if (
|
|
||||||
entry.name.endsWith(".onnx") &&
|
|
||||||
fs.statSync(full).size < 1024
|
|
||||||
) {
|
|
||||||
console.warn(
|
|
||||||
"[WhisperWorker] Removing likely-corrupt file:",
|
|
||||||
full,
|
|
||||||
);
|
|
||||||
fs.unlinkSync(full);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
walk(modelDir);
|
|
||||||
}
|
|
||||||
} catch (cleanErr) {
|
|
||||||
console.warn("[WhisperWorker] Cache cleanup error:", cleanErr.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const { pipeline, env } = await import("@huggingface/transformers");
|
|
||||||
env.cacheDir = cacheDir;
|
|
||||||
|
|
||||||
// Attempt to load with the requested device
|
|
||||||
const devicesToTry = device === "wasm" ? ["wasm"] : ["cpu", "wasm"];
|
|
||||||
|
|
||||||
let lastError = null;
|
|
||||||
|
|
||||||
for (const dev of devicesToTry) {
|
|
||||||
try {
|
|
||||||
send({
|
|
||||||
type: "status",
|
|
||||||
message: `Loading Whisper (${dev} backend)...`,
|
|
||||||
});
|
|
||||||
console.log(
|
|
||||||
`[WhisperWorker] Trying device: ${dev}, model: ${modelName}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
whisperPipeline = await pipeline(
|
|
||||||
"automatic-speech-recognition",
|
|
||||||
modelName,
|
|
||||||
{
|
|
||||||
dtype: "q8",
|
|
||||||
device: dev,
|
|
||||||
progress_callback: (progress) => {
|
|
||||||
// progress: { status, name?, file?, progress?, loaded?, total? }
|
|
||||||
if (
|
|
||||||
progress.status === "download" ||
|
|
||||||
progress.status === "progress"
|
|
||||||
) {
|
|
||||||
send({
|
|
||||||
type: "progress",
|
|
||||||
file: progress.file || progress.name || "",
|
|
||||||
progress: progress.progress ?? 0,
|
|
||||||
loaded: progress.loaded ?? 0,
|
|
||||||
total: progress.total ?? 0,
|
|
||||||
status: progress.status,
|
|
||||||
});
|
|
||||||
} else if (progress.status === "done") {
|
|
||||||
send({
|
|
||||||
type: "progress",
|
|
||||||
file: progress.file || progress.name || "",
|
|
||||||
progress: 100,
|
|
||||||
loaded: progress.total ?? 0,
|
|
||||||
total: progress.total ?? 0,
|
|
||||||
status: "done",
|
|
||||||
});
|
|
||||||
} else if (progress.status === "initiate") {
|
|
||||||
send({
|
|
||||||
type: "progress",
|
|
||||||
file: progress.file || progress.name || "",
|
|
||||||
progress: 0,
|
|
||||||
loaded: 0,
|
|
||||||
total: 0,
|
|
||||||
status: "initiate",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
activeDevice = dev;
|
|
||||||
console.log(
|
|
||||||
`[WhisperWorker] Model loaded successfully (device: ${dev})`,
|
|
||||||
);
|
|
||||||
send({ type: "load-result", success: true, device: dev });
|
|
||||||
return;
|
|
||||||
} catch (err) {
|
|
||||||
lastError = err;
|
|
||||||
console.error(
|
|
||||||
`[WhisperWorker] Failed to load with device "${dev}":`,
|
|
||||||
err.message || err,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (dev === "cpu" && devicesToTry.includes("wasm")) {
|
|
||||||
send({
|
|
||||||
type: "status",
|
|
||||||
message: `Native CPU backend failed (${err.message}). Trying WASM fallback...`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset pipeline state before retry
|
|
||||||
whisperPipeline = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// All devices failed
|
|
||||||
throw lastError || new Error("All ONNX backends failed");
|
|
||||||
} catch (error) {
|
|
||||||
send({ type: "load-result", success: false, error: error.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function transcribe(audioBase64, language) {
|
|
||||||
if (!whisperPipeline) {
|
|
||||||
send({
|
|
||||||
type: "transcribe-result",
|
|
||||||
success: false,
|
|
||||||
error: "Whisper pipeline not loaded",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const pcm16Buffer = Buffer.from(audioBase64, "base64");
|
|
||||||
|
|
||||||
if (pcm16Buffer.length < 2) {
|
|
||||||
send({
|
|
||||||
type: "transcribe-result",
|
|
||||||
success: false,
|
|
||||||
error: "Audio buffer too small",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cap at ~30 seconds (16kHz, 16-bit mono)
|
|
||||||
const maxBytes = 16000 * 2 * 30;
|
|
||||||
const audioData =
|
|
||||||
pcm16Buffer.length > maxBytes
|
|
||||||
? pcm16Buffer.slice(0, maxBytes)
|
|
||||||
: pcm16Buffer;
|
|
||||||
|
|
||||||
const float32Audio = pcm16ToFloat32(audioData);
|
|
||||||
if (float32Audio.length === 0) {
|
|
||||||
send({
|
|
||||||
type: "transcribe-result",
|
|
||||||
success: false,
|
|
||||||
error: "Empty audio after conversion",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build pipeline options with the requested language
|
|
||||||
const pipelineOpts = {
|
|
||||||
sampling_rate: 16000,
|
|
||||||
task: "transcribe",
|
|
||||||
};
|
|
||||||
if (language && language !== "auto") {
|
|
||||||
pipelineOpts.language = language;
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await whisperPipeline(float32Audio, pipelineOpts);
|
|
||||||
|
|
||||||
const text = result.text?.trim() || "";
|
|
||||||
send({ type: "transcribe-result", success: true, text });
|
|
||||||
} catch (error) {
|
|
||||||
send({
|
|
||||||
type: "transcribe-result",
|
|
||||||
success: false,
|
|
||||||
error: error.message || String(error),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function send(msg) {
|
|
||||||
try {
|
|
||||||
if (process.send) {
|
|
||||||
process.send(msg);
|
|
||||||
}
|
|
||||||
} catch (_) {
|
|
||||||
// Parent may have disconnected
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
process.on("message", (msg) => {
|
|
||||||
switch (msg.type) {
|
|
||||||
case "load":
|
|
||||||
loadModel(msg.modelName, msg.cacheDir, msg.device).catch((err) => {
|
|
||||||
send({ type: "load-result", success: false, error: err.message });
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case "transcribe":
|
|
||||||
transcribe(msg.audioBase64, msg.language).catch((err) => {
|
|
||||||
send({ type: "transcribe-result", success: false, error: err.message });
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case "shutdown":
|
|
||||||
// Dispose the ONNX session gracefully before exiting to avoid
|
|
||||||
// native cleanup race conditions (SIGABRT on mutex destroy).
|
|
||||||
(async () => {
|
|
||||||
if (whisperPipeline) {
|
|
||||||
try {
|
|
||||||
if (typeof whisperPipeline.dispose === "function") {
|
|
||||||
await whisperPipeline.dispose();
|
|
||||||
}
|
|
||||||
} catch (_) {
|
|
||||||
// Best-effort cleanup
|
|
||||||
}
|
|
||||||
whisperPipeline = null;
|
|
||||||
}
|
|
||||||
// Small delay to let native threads wind down
|
|
||||||
setTimeout(() => process.exit(0), 200);
|
|
||||||
})();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Signal readiness to parent
|
|
||||||
send({ type: "ready" });
|
|
||||||
Loading…
x
Reference in New Issue
Block a user