@ -1,94 +1,46 @@
const { app , BrowserWindow , ipcMain , dialog } = require ( "electron" ) ;
const pdfParse = require ( "pdf-parse" ) ;
const mammoth = require ( "mammoth" ) ;
const fs = require ( "fs" ) ;
const fs = require ( "fs" ) . promises ;
const path = require ( "path" ) ;
const os = require ( "os" ) ;
const { queryOpenAI } = require ( "./chatgpt.js" ) ;
const OpenAI = require ( "openai" ) ;
const Ollama = require ( "ollama" ) ;
const Anthropic = require ( "@anthropic-ai/sdk" ) ;
const axios = require ( "axios" ) ;
const fsExtra = require ( "fs-extra" ) ;
const fsConstants = require ( "fs" ) . constants ;
let fetch , allModels ;
let fetch ;
import ( "node-fetch" ) . then ( ( module ) => {
fetch = module . default ;
} ) ;
const unzipper = require ( "unzipper" ) ;
let win ;
let openai ;
let ollama = new Ollama . Ollama ( ) ;
function promptUserForApiKey ( ) {
// Create a new window to prompt the user for the API key
const promptWindow = new BrowserWindow ( {
// Window configuration for the prompt
width : 500 ,
height : 200 ,
webPreferences : {
nodeIntegration : true ,
contextIsolation : false , // Consider security implications
} ,
} ) ;
// Handle the API key submission from the prompt window
ipcMain . on ( "submit-api-key" , ( event , apiKey ) => {
if ( apiKey ) {
saveApiKey ( apiKey ) ;
promptWindow . close ( ) ;
createWindow ( ) ; // Proceed to create the main window
} else {
// Handle invalid input or user cancellation
promptWindow . close ( ) ;
}
} ) ;
}
function loadApiKey ( ) {
const configPath = path . join ( os . homedir ( ) , ".config" , "fabric" , ".env" ) ;
if ( fs . existsSync ( configPath ) ) {
const envContents = fs . readFileSync ( configPath , { encoding : "utf8" } ) ;
const matches = envContents . match ( /^OPENAI_API_KEY=(.*)$/m ) ;
if ( matches && matches [ 1 ] ) {
return matches [ 1 ] ;
}
}
return null ;
}
function saveApiKey ( apiKey ) {
const configPath = path . join ( os . homedir ( ) , ".config" , "fabric" ) ;
const envFilePath = path . join ( configPath , ".env" ) ;
if ( ! fs . existsSync ( configPath ) ) {
fs . mkdirSync ( configPath , { recursive : true } ) ;
}
fs . writeFileSync ( envFilePath , ` OPENAI_API_KEY= ${ apiKey } ` ) ;
process . env . OPENAI _API _KEY = apiKey ; // Set for current session
}
function ensureFabricFoldersExist ( ) {
return new Promise ( async ( resolve , reject ) => {
async function ensureFabricFoldersExist ( ) {
const fabricPath = path . join ( os . homedir ( ) , ".config" , "fabric" ) ;
const patternsPath = path . join ( fabricPath , "patterns" ) ;
try {
if ( ! fs . existsSync ( fabricPath ) ) {
fs . mkdirSync ( fabricPath , { recursive : true } ) ;
}
if ( ! fs . existsSync ( patternsPath ) ) {
fs . mkdirSync ( patternsPath , { recursive : true } ) ;
await downloadAndUpdatePatterns ( patternsPath ) ;
}
resolve ( ) ; // Resolve the promise once everything is set up
await fs
. access ( fabricPath , fsConstants . F _OK )
. catch ( ( ) => fs . mkdir ( fabricPath , { recursive : true } ) ) ;
await fs
. access ( patternsPath , fsConstants . F _OK )
. catch ( ( ) => fs . mkdir ( patternsPath , { recursive : true } ) ) ;
// Optionally download and update patterns after ensuring the directories exist
} catch ( error ) {
console . error ( "Error ensuring fabric folders exist:" , error ) ;
reject ( error ) ; // Reject the promise if an error occurs
throw error ; // Make sure to re-throw the error to handle it further up the call stack if necessary
}
} ) ;
}
async function downloadAndUpdatePatterns ( patternsPath ) {
async function downloadAndUpdatePatterns ( ) {
try {
// Download the zip file
const response = await axios ( {
method : "get" ,
url : "https://github.com/danielmiessler/fabric/archive/refs/heads/main.zip" ,
@ -99,16 +51,15 @@ async function downloadAndUpdatePatterns(patternsPath) {
fs . writeFileSync ( zipPath , response . data ) ;
console . log ( "Zip file written to:" , zipPath ) ;
// Prepare for extraction
const tempExtractPath = path . join ( os . tmpdir ( ) , "fabric_extracted" ) ;
fsExtra . emptyDirSync ( tempExtractPath ) ;
await fsExtra . remove ( patternsPath ) ; // Delete the existing patterns directory
await fsExtra . emptyDir ( tempExtractPath ) ;
// Extract the zip file
await fs
. createReadStream ( zipPath )
. pipe ( unzipper . Extract ( { path : tempExtractPath } ) )
. promise ( ) ;
console . log ( "Extraction complete" ) ;
const extractedPatternsPath = path . join (
@ -117,30 +68,207 @@ async function downloadAndUpdatePatterns(patternsPath) {
"patterns"
) ;
await fsExtra . copy ( extractedPatternsPath , patternsPath ) ;
// Compare and move folders
const existingPatternsPath = path . join (
os . homedir ( ) ,
".config" ,
"fabric" ,
"patterns"
) ;
if ( fs . existsSync ( existingPatternsPath ) ) {
const existingFolders = await fsExtra . readdir ( existingPatternsPath ) ;
for ( const folder of existingFolders ) {
if ( ! fs . existsSync ( path . join ( extractedPatternsPath , folder ) ) ) {
await fsExtra . move (
path . join ( existingPatternsPath , folder ) ,
path . join ( extractedPatternsPath , folder )
) ;
console . log (
` Moved missing folder ${ folder } to the extracted patterns directory. `
) ;
}
}
}
// Overwrite the existing patterns directory with the updated extracted directory
await fsExtra . copy ( extractedPatternsPath , existingPatternsPath , {
overwrite : true ,
} ) ;
console . log ( "Patterns successfully updated" ) ;
// Inform the renderer process that the patterns have been updated
win . webContents . send ( "patterns-updated" ) ;
// win.webContents.send("patterns-updated");
} catch ( error ) {
console . error ( "Error downloading or updating patterns:" , error ) ;
}
}
function checkApiKeyExists ( ) {
const configPath = path . join ( os . homedir ( ) , ".config" , "fabric" , ".env" ) ;
return fs . existsSync ( configPath ) ;
}
function getPatternFolders ( ) {
const patternsPath = path . join ( os . homedir ( ) , ".config" , "fabric" , "patterns" ) ;
return fs
. readdirSync ( patternsPath , { withFileTypes : true } )
return new Promise ( ( resolve , reject ) => {
fs . readdir ( patternsPath , { withFileTypes : true } , ( error , dirents ) => {
if ( error ) {
console . error ( "Failed to read pattern folders:" , error ) ;
reject ( error ) ;
} else {
const folders = dirents
. filter ( ( dirent ) => dirent . isDirectory ( ) )
. map ( ( dirent ) => dirent . name ) ;
resolve ( folders ) ;
}
} ) ;
} ) ;
}
async function checkApiKeyExists ( ) {
const configPath = path . join ( os . homedir ( ) , ".config" , "fabric" , ".env" ) ;
try {
await fs . access ( configPath , fsConstants . F _OK ) ;
return true ; // The file exists
} catch ( e ) {
return false ; // The file does not exist
}
}
async function loadApiKeys ( ) {
const configPath = path . join ( os . homedir ( ) , ".config" , "fabric" , ".env" ) ;
let keys = { openAIKey : null , claudeKey : null } ;
try {
const envContents = await fs . readFile ( configPath , { encoding : "utf8" } ) ;
const openAIMatch = envContents . match ( /^OPENAI_API_KEY=(.*)$/m ) ;
const claudeMatch = envContents . match ( /^CLAUDE_API_KEY=(.*)$/m ) ;
if ( openAIMatch && openAIMatch [ 1 ] ) {
keys . openAIKey = openAIMatch [ 1 ] ;
}
if ( claudeMatch && claudeMatch [ 1 ] ) {
keys . claudeKey = claudeMatch [ 1 ] ;
claude = new Anthropic ( { apiKey : keys . claudeKey } ) ;
}
} catch ( error ) {
console . error ( "Could not load API keys:" , error ) ;
}
return keys ;
}
async function saveApiKeys ( openAIKey , claudeKey ) {
const configPath = path . join ( os . homedir ( ) , ".config" , "fabric" ) ;
const envFilePath = path . join ( configPath , ".env" ) ;
try {
await fs . access ( configPath ) ;
} catch {
await fs . mkdir ( configPath , { recursive : true } ) ;
}
let envContent = "" ;
// Read the existing .env file if it exists
try {
envContent = await fs . readFile ( envFilePath , "utf8" ) ;
} catch ( err ) {
if ( err . code !== "ENOENT" ) {
throw err ;
}
// If the file doesn't exist, create an empty .env file
await fs . writeFile ( envFilePath , "" ) ;
}
// Update the specific API key
if ( openAIKey ) {
envContent = updateOrAddKey ( envContent , "OPENAI_API_KEY" , openAIKey ) ;
process . env . OPENAI _API _KEY = openAIKey ; // Set for current session
openai = new OpenAI ( { apiKey : openAIKey } ) ;
}
if ( claudeKey ) {
envContent = updateOrAddKey ( envContent , "CLAUDE_API_KEY" , claudeKey ) ;
process . env . CLAUDE _API _KEY = claudeKey ; // Set for current session
claude = new Anthropic ( { apiKey : claudeKey } ) ;
}
await fs . writeFile ( envFilePath , envContent . trim ( ) ) ;
await loadApiKeys ( ) ;
win . webContents . send ( "api-keys-saved" ) ;
}
function updateOrAddKey ( envContent , keyName , keyValue ) {
const keyPattern = new RegExp ( ` ^ ${ keyName } =.* $ ` , "m" ) ;
if ( keyPattern . test ( envContent ) ) {
// Update the existing key
envContent = envContent . replace ( keyPattern , ` ${ keyName } = ${ keyValue } ` ) ;
} else {
// Add the new key
envContent += ` \n ${ keyName } = ${ keyValue } ` ;
}
return envContent ;
}
function getPatternContent ( patternName ) {
async function getOllamaModels ( ) {
try {
ollama = new Ollama . Ollama ( ) ;
const _models = await ollama . list ( ) ;
return _models . models . map ( ( x ) => x . name ) ;
} catch ( error ) {
if ( error . cause && error . cause . code === "ECONNREFUSED" ) {
console . error (
"Failed to connect to Ollama. Make sure Ollama is running and accessible."
) ;
return [ ] ; // Return an empty array instead of throwing an error
} else {
console . error ( "Error fetching models from Ollama:" , error ) ;
throw error ; // Re-throw the error for other types of errors
}
}
}
async function getModels ( ) {
allModels = {
gptModels : [ ] ,
claudeModels : [ ] ,
ollamaModels : [ ] ,
} ;
let keys = await loadApiKeys ( ) ;
if ( keys . claudeKey ) {
claudeModels = [
"claude-3-opus-20240229" ,
"claude-3-sonnet-20240229" ,
"claude-3-haiku-20240307" ,
"claude-2.1" ,
] ;
allModels . claudeModels = claudeModels ;
}
if ( keys . openAIKey ) {
openai = new OpenAI ( { apiKey : keys . openAIKey } ) ;
try {
const response = await openai . models . list ( ) ;
allModels . gptModels = response . data ;
} catch ( error ) {
console . error ( "Error fetching models from OpenAI:" , error ) ;
}
}
// Check if ollama exists and has a list method
if (
typeof ollama !== "undefined" &&
ollama . list &&
typeof ollama . list === "function"
) {
try {
allModels . ollamaModels = await getOllamaModels ( ) ;
} catch ( error ) {
console . error ( "Error fetching models from Ollama:" , error ) ;
}
} else {
console . log ( "Ollama is not available or does not support listing models." ) ;
}
return allModels ;
}
async function getPatternContent ( patternName ) {
const patternPath = path . join (
os . homedir ( ) ,
".config" ,
@ -150,13 +278,119 @@ function getPatternContent(patternName) {
"system.md"
) ;
try {
return fs . readFileSync ( patternPath , "utf8" ) ;
const content = await fs . readFile ( patternPath , "utf8" ) ;
return content ;
} catch ( error ) {
console . error ( "Error reading pattern file:" , error ) ;
return "" ;
}
}
async function ollamaMessage ( system , user , model , event ) {
ollama = new Ollama . Ollama ( ) ;
const userMessage = {
role : "user" ,
content : user ,
} ;
const systemMessage = { role : "system" , content : system } ;
const response = await ollama . chat ( {
model : model ,
messages : [ systemMessage , userMessage ] ,
stream : true ,
} ) ;
let responseMessage = "" ;
for await ( const chunk of response ) {
const content = chunk . message . content ;
if ( content ) {
responseMessage += content ;
event . reply ( "model-response" , content ) ;
}
event . reply ( "model-response-end" , responseMessage ) ;
}
}
async function openaiMessage ( system , user , model , event ) {
const userMessage = { role : "user" , content : user } ;
const systemMessage = { role : "system" , content : system } ;
const stream = await openai . chat . completions . create (
{
model : model ,
messages : [ systemMessage , userMessage ] ,
stream : true ,
} ,
{ responseType : "stream" }
) ;
let responseMessage = "" ;
for await ( const chunk of stream ) {
const content = chunk . choices [ 0 ] . delta . content ;
if ( content ) {
responseMessage += content ;
event . reply ( "model-response" , content ) ;
}
}
event . reply ( "model-response-end" , responseMessage ) ;
}
async function claudeMessage ( system , user , model , event ) {
if ( ! claude ) {
event . reply (
"model-response-error" ,
"Claude API key is missing or invalid."
) ;
return ;
}
const userMessage = { role : "user" , content : user } ;
const systemMessage = system ;
const response = await claude . messages . create ( {
model : model ,
system : systemMessage ,
max _tokens : 4096 ,
messages : [ userMessage ] ,
stream : true ,
temperature : 0.0 ,
top _p : 1.0 ,
} ) ;
let responseMessage = "" ;
for await ( const chunk of response ) {
if ( chunk . delta && chunk . delta . text ) {
responseMessage += chunk . delta . text ;
event . reply ( "model-response" , chunk . delta . text ) ;
}
}
event . reply ( "model-response-end" , responseMessage ) ;
}
async function createPatternFolder ( patternName , patternBody ) {
try {
const patternsPath = path . join (
os . homedir ( ) ,
".config" ,
"fabric" ,
"patterns"
) ;
const patternFolderPath = path . join ( patternsPath , patternName ) ;
// Create the pattern folder using the promise-based API
await fs . mkdir ( patternFolderPath , { recursive : true } ) ;
// Create the system.md file inside the pattern folder
const filePath = path . join ( patternFolderPath , "system.md" ) ;
await fs . writeFile ( filePath , patternBody ) ;
console . log (
` Pattern folder ' ${ patternName } ' created successfully with system.md inside. `
) ;
return ` Pattern folder ' ${ patternName } ' created successfully with system.md inside. ` ;
} catch ( err ) {
console . error ( ` Failed to create the pattern folder: ${ err . message } ` ) ;
throw err ; // Ensure the error is thrown so it can be caught by the caller
}
}
function createWindow ( ) {
win = new BrowserWindow ( {
width : 800 ,
@ -174,57 +408,49 @@ function createWindow() {
win = null ;
} ) ;
}
ipcMain . on ( "process-complex-file" , ( event , filePath ) => {
const extension = path . extname ( filePath ) . toLowerCase ( ) ;
let fileProcessPromise ;
if ( extension === ".pdf" ) {
const dataBuffer = fs . readFileSync ( filePath ) ;
fileProcessPromise = pdfParse ( dataBuffer ) . then ( ( data ) => data . text ) ;
} else if ( extension === ".docx" ) {
fileProcessPromise = mammoth
. extractRawText ( { path : filePath } )
. then ( ( result ) => result . value )
. catch ( ( err ) => {
console . error ( "Error processing DOCX file:" , err ) ;
throw new Error ( "Error processing DOCX file." ) ;
} ) ;
} else {
event . reply ( "file-response" , "Error: Unsupported file type" ) ;
ipcMain . on ( "start-query" , async ( event , system , user , model ) => {
if ( system == null || user == null || model == null ) {
console . error ( "Received null for system, user message, or model" ) ;
event . reply (
"model-response-error" ,
"Error: System, user message, or model is null."
) ;
return ;
}
fileProcessPromise
. then ( ( extractedText ) => {
// Sending the extracted text back to the frontend.
event . reply ( "file-response" , extractedText ) ;
} )
. catch ( ( error ) => {
// Handling any errors during file processing and sending them back to the frontend.
event . reply ( "file-response" , ` Error processing file: ${ error . message } ` ) ;
} ) ;
try {
const _gptModels = allModels . gptModels . map ( ( model ) => model . id ) ;
if ( allModels . claudeModels . includes ( model ) ) {
await claudeMessage ( system , user , model , event ) ;
} else if ( _gptModels . includes ( model ) ) {
await openaiMessage ( system , user , model , event ) ;
} else if ( allModels . ollamaModels . includes ( model ) ) {
await ollamaMessage ( system , user , model , event ) ;
} else {
event . reply ( "model-response-error" , "Unsupported model: " + model ) ;
}
} catch ( error ) {
console . error ( "Error querying model:" , error ) ;
event . reply ( "model-response-error" , "Error querying model." ) ;
}
} ) ;
ipcMain . on ( "start-query-openai" , async ( event , system , user ) => {
if ( system == null || user == null ) {
console . error ( "Received null for system or user message" ) ;
event . reply ( "openai-response" , "Error: System or user message is null." ) ;
return ;
}
ipcMain . handle ( "create-pattern" , async ( event , patternName , patternContent ) => {
try {
await queryOpenAI ( system , user , ( message ) => {
event . reply ( "openai-response" , message ) ;
} ) ;
const result = await createPatternFolder ( patternName , patternContent ) ;
return { status : "success" , message : result } ; // Use a response object for more detailed responses
} catch ( error ) {
console . error ( "Error querying OpenAI :" , error ) ;
event . reply ( "no-api-key" , "Error querying OpenAI." ) ;
console . error ( "Error creating pattern:" , error ) ;
return { status : "error" , message : error . message } ; // Return an error object
}
} ) ;
// Example of using ipcMain.handle for asynchronous operations
ipcMain . handle ( "get-patterns" , async ( event ) => {
try {
return getPatternFolders ( ) ;
const patterns = await getPatternFolders ( ) ;
return patterns ;
} catch ( error ) {
console . error ( "Failed to get patterns:" , error ) ;
return [ ] ;
@ -238,51 +464,43 @@ ipcMain.on("update-patterns", () => {
ipcMain . handle ( "get-pattern-content" , async ( event , patternName ) => {
try {
return getPatternContent ( patternName ) ;
const content = await getPatternContent ( patternName ) ;
return content ;
} catch ( error ) {
console . error ( "Failed to get pattern content:" , error ) ;
return "" ;
}
} ) ;
ipcMain . handle ( "save-api-key" , async ( event , apiKey ) => {
ipcMain . handle ( "save-api-keys " , async ( event , { openAIKey , claudeKey } ) => {
try {
const configPath = path . join ( os . homedir ( ) , ".config" , "fabric" ) ;
if ( ! fs . existsSync ( configPath ) ) {
fs . mkdirSync ( configPath , { recursive : true } ) ;
await saveApiKeys ( openAIKey , claudeKey ) ;
return "API Keys saved successfully." ;
} catch ( error ) {
console . error ( "Error saving API keys:" , error ) ;
throw new Error ( "Failed to save API Keys." ) ;
}
} ) ;
const envFilePath = path . join ( configPath , ".env" ) ;
fs . writeFileSync ( envFilePath , ` OPENAI_API_KEY= ${ apiKey } ` ) ;
process . env . OPENAI _API _KEY = apiKey ;
return "API Key saved successfully." ;
ipcMain . handle ( "get-models" , async ( event ) => {
try {
const models = await getModels ( ) ;
return models ;
} catch ( error ) {
console . error ( "Error saving API key :" , error ) ;
throw new Error ( "Failed to save API Key." ) ;
console . error ( "Failed to get models :" , error ) ;
return { gptModels : [ ] , claudeModels : [ ] , ollamaModels : [ ] } ;
}
} ) ;
app . whenReady ( ) . then ( async ( ) => {
try {
const apiKey = loadApiKey ( ) ;
if ( ! apiKey ) {
promptUserForApiKey ( ) ;
} else {
process . env . OPENAI _API _KEY = apiKey ;
createWindow ( ) ;
}
const keys = await loadApiKeys ( ) ;
await ensureFabricFoldersExist ( ) ; // Ensure fabric folders exist
createWindow ( ) ; // Create the application window
// After window creation, check if the API key exists
if ( ! checkApiKeyExists ( ) ) {
console . log ( "API key is missing. Prompting user to input API key." ) ;
// Optionally, directly invoke a function here to show a prompt in the renderer process
win . webContents . send ( "request-api-key" ) ;
}
await getModels ( ) ; // Fetch models after loading API keys
createWindow ( ) ; // Keep this line
} catch ( error ) {
console . error ( "Failed to initialize fabric folders:" , error ) ;
await ensureFabricFoldersExist ( ) ; // Ensure fabric folders exist
createWindow ( ) ; // Keep this line
// Handle initialization failure (e.g., close the app or show an error message)
}
} ) ;