Compare commits

..

25 Commits
0.01 ... master

Author SHA1 Message Date
Wizzard 045f78d50d Add code blocks and resizable window 2024-03-09 17:23:16 -05:00
Wizzard ee6f775b5b Restructure GUI folder & modify styles.css 2024-03-09 16:19:47 -05:00
Wizzard bdf5715868 Add stop button 2024-03-09 15:22:44 -05:00
Wizzard d2470b2dd6 Update GUI styles.css & increase prompt timeout in GUI & CLI 2024-03-09 13:53:26 -05:00
Wizzard 05e5495fad Update README.md 2024-03-09 12:28:19 -05:00
Wizzard 5298482708 Update workflow 2024-03-09 12:02:42 -05:00
Wizzard 113e413167 Update cli.yml and gui.yml 2024-03-09 11:56:46 -05:00
Wizzard 5bc5fb7cad Bump nodejs version in gui.yml & create cli.yml 2024-03-09 11:46:27 -05:00
Wizzard 9a92b47a02 Retry creating gui.yml 2024-03-09 11:32:33 -05:00
Wizzard 81a8f6b9e3 Move gui.yml to .github/workflows 2024-03-09 11:17:46 -05:00
Wizzard 35309ee2d1 Add gui.yml 2024-03-09 11:14:43 -05:00
Wizzard 445eda821d Include timeout function in GUI 2024-03-09 05:56:54 -05:00
Wizzard 437831aea5 Create dropdown box to select which model to use in the GUI on startup 2024-03-09 05:43:40 -05:00
Wizzard e5419b0108 Update kuzco-cli to allow choice of model on startup & include a timeout function 2024-03-09 04:50:30 -05:00
Wizzard 424a1b5ad1 Update README.md 2024-03-09 03:59:07 -05:00
Wizzard 1a00e34837 Update package.json 2024-03-09 03:33:54 -05:00
Wizzard 2d9f8649a7 Update README.md 2024-03-09 03:26:06 -05:00
Wizzard 16c1d680cb Typing indicator for GUI 2024-03-09 03:20:24 -05:00
Wizzard 0ab585943f Wait for AI response before letting user send another message in GUI 2024-03-09 03:12:19 -05:00
Wizzard 226f4bf409 Update package.json 2024-03-09 02:57:57 -05:00
Wizzard 4bc3b013bb Restart GUI when we get API key 2024-03-09 02:52:10 -05:00
Wizzard 97402a8b1f Add function to alert user if their API key isnt set and open a popup for user to enter their key 2024-03-09 02:42:30 -05:00
Wizzard 61cf0bd49e move kuzco-cli.js 2024-03-09 02:18:37 -05:00
Wizzard af39ad7094 Restructure project 2024-03-09 02:17:26 -05:00
Wizzard fd8bba8199 First GUI version 2024-03-09 02:15:01 -05:00
16 changed files with 4300 additions and 16 deletions

51
.github/workflows/cli.yml vendored Normal file
View File

@ -0,0 +1,51 @@
name: CLI Build
on: [push, pull_request]
jobs:
build-cli:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies (CLI)
run: cd cli && npm install
- name: Install pkg globally
run: npm install -g pkg
- name: Build CLI executables
run: |
cd cli
pkg kuzco-cli.js --targets node18-linux-x64,node18-macos-x64,node18-win-x64
- name: Rename executables
run: |
cd cli
mv kuzco-cli-linux kuzco-cli-linux-x64
mv kuzco-cli-macos kuzco-cli-macos-x64
mv kuzco-cli-win.exe kuzco-cli-win-x64.exe
- name: Upload Linux Executable
uses: actions/upload-artifact@v4
with:
name: kuzco-cli-linux-x64
path: cli/kuzco-cli-linux-x64
- name: Upload macOS Executable
uses: actions/upload-artifact@v4
with:
name: kuzco-cli-macos-x64
path: cli/kuzco-cli-macos-x64
- name: Upload Windows Executable
uses: actions/upload-artifact@v4
with:
name: kuzco-cli-win-x64.exe
path: cli/kuzco-cli-win-x64.exe

75
.github/workflows/gui.yml vendored Normal file
View File

@ -0,0 +1,75 @@
name: GUI Build
on: [push, pull_request]
jobs:
build-linux-gui:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies (GUI)
run: cd gui && npm install
- name: Build Linux Application
run: cd gui && npm run dist-linux
- name: Archive Linux production build
run: |
cd gui/dist
tar czf linux-unpacked.tar.gz linux-unpacked
- name: Upload Linux AppImage
uses: actions/upload-artifact@v4
with:
name: KuzcoChat-Linux-Appimage
path: gui/dist/*.AppImage
- name: Upload Linux DEB
uses: actions/upload-artifact@v4
with:
name: KuzcoChat-DEB
path: gui/dist/*.deb
- name: Upload Linux Unpacked Archive
uses: actions/upload-artifact@v4
with:
name: KuzcoChat-Linux-Unpacked
path: gui/dist/linux-unpacked.tar.gz
build-windows-gui:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies (GUI)
run: cd gui && npm install
- name: Build Windows Application
run: cd gui && npm run dist-win
- name: Zip Windows production build
run: |
Compress-Archive -Path gui/dist/win-unpacked/* -DestinationPath gui/dist/win-unpacked.zip
- name: Upload Windows Executable
uses: actions/upload-artifact@v4
with:
name: KuzcoChat-Windows-EXE
path: gui/dist/*.exe
- name: Upload Windows Unpacked Archive
uses: actions/upload-artifact@v4
with:
name: KuzcoChat-Windows-Unpacked
path: gui/dist/win-unpacked.zip

6
.gitignore vendored
View File

@ -1,4 +1,4 @@
node_modules/
main-linux
main-macos
*.exe
gui/dist/
*.exe

View File

@ -19,7 +19,7 @@ Welcome to Kuzco CLI, a sleek Node.js-based interface for interacting with Kuzco
1. Clone this repository and navigate into the directory:
```bash
git clone https://git.deadzone.lol/Wizzard/kuzco-cli.git
cd kuzco-cli
cd kuzco-cli/cli
```
2. Install the necessary Node.js dependencies:
@ -27,6 +27,16 @@ Welcome to Kuzco CLI, a sleek Node.js-based interface for interacting with Kuzco
npm install
```
3. Installing the GUI:
```bash
cd kuzco-cli/gui
```
4. Install the necessary Node.js dependencies:
```bash
npm install
```
### Configuration
On first run, you'll be prompted to enter your API key for accessing Kuzco's network. This is stored securely and ensures your interactions are authenticated.
@ -35,17 +45,26 @@ On first run, you'll be prompted to enter your API key for accessing Kuzco's net
Run Kuzco CLI with:
```bash
node main.js
cd kuzco-cli/cli
node kuzco-cli.js
```
Run Kuzco GUI with:
```bash
cd kuzco-cli/gui
electron19 kuzco-gui.js
```
Follow the on-screen prompts to send your AI prompts to the network.
## Prebuilt Binaries
You can download prebuilt versions of this application from the [GitHub Actions page](https://github.com/CODJointOps/kuzco-cli/actions).
## Contributing
Your contributions are welcome!
## License
Kuzco CLI is released under the MIT license. Feel free to use, modify, and distribute it as you see fit.
Enjoy harnessing the power of decentralized AI with Kuzco!
Kuzco CLI is released under the MIT license. Feel free to use, modify, and distribute it as you see fit.

View File

@ -52,9 +52,27 @@ const askQuestion = (query) => {
}));
};
const fetchWithTimeout = (url, options, timeout = 3000) => {
const timeoutPromise = new Promise((_, reject) => {
const id = setTimeout(() => {
clearTimeout(id);
reject(new Error('Request timed out'));
}, timeout);
});
return Promise.race([
fetch(url, options),
timeoutPromise
]);
};
async function main() {
let messages = [];
console.log("Please choose a model: 1 for Mistral, 2 for Llama2");
const modelChoice = prompt('Enter your choice (1 or 2): ');
const model = modelChoice === '2' ? 'llama2' : 'mistral';
while (true) {
const user_input = await askQuestion("User: ");
if (user_input.toLowerCase() === 'exit') {
@ -63,7 +81,7 @@ async function main() {
messages.push({ 'role': 'user', 'content': user_input + '\n' });
try {
const response = await fetch(`${BASE_URL}/chat/completions`, {
const response = await fetchWithTimeout(`${BASE_URL}/chat/completions`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
@ -71,17 +89,16 @@ async function main() {
},
body: JSON.stringify({
messages: messages,
model: 'mistral',
stream: false
model: model
})
});
}, 25000);;
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log(`\nKuzco (Mistral):\n\n${data.choices[0].message.content.trim()}\n`);
console.log(`\nKuzco (${model}):\n\n${data.choices[0].message.content.trim()}\n`);
} catch (error) {
console.error(`An error occurred: ${error.message}`);
}

View File

@ -1,11 +1,11 @@
{
"name": "kyzco-cli",
"name": "kuzco-cli",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "kyzco-cli",
"name": "kuzco-cli",
"version": "1.0.0",
"dependencies": {
"got": "^14.2.1",
@ -13,7 +13,7 @@
"prompt-sync": "^4.2.0"
},
"bin": {
"kyzco-cli": "kuzco-cli.js"
"kuzco-cli": "kuzco-cli.js"
}
},
"node_modules/@sindresorhus/is": {

222
gui/css/styles.css Normal file
View File

@ -0,0 +1,222 @@
body {
background-color: #2c3e50;
color: #ecf0f1;
font-family: Arial, sans-serif;
display: flex;
flex-direction: column;
height: 100vh;
margin: 0;
padding: 20px;
box-sizing: border-box;
scrollbar-width: thin;
scrollbar-color: #34495e #2c3e50;
}
body, html {
height: 100%;
margin: 0;
}
#app {
display: flex;
flex-direction: column;
height: 100%;
}
#chatHistory {
height: 300px;
flex-grow: 1;
overflow-y: auto;
padding: 10px;
margin-bottom: 20px;
border: 1px solid #34495e;
border-radius: 5px;
}
#sendButton {
padding: 10px 20px;
font-size: 1rem;
color: white;
background-color: #007BFF;
border: none;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.2s;
}
#sendButton:hover {
background-color: #0056b3;
}
#sendButton:disabled {
background-color: #6c757d;
cursor: not-allowed;
}
#submitApiKey {
padding: 10px 20px;
font-size: 1rem;
color: white;
background-color: #007BFF;
border: none;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.2s;
}
#stopButton {
padding: 10px 20px;
font-size: 1rem;
color: white;
background-color: #c0392b;
border: none;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.2s;
}
#stopButton:hover {
background-color: #a93226;
}
#stopButton:disabled {
background-color: #6c757d;
cursor: not-allowed;
}
.message {
font-size: 0.9em;
padding: 10px 15px;
margin-bottom: 15px;
padding: 10px;
background-color: #34495e;
border-radius: 5px;
}
.userMessage {
align-self: flex-end;
background-color: #2980b9;
}
.assistantMessage {
align-self: flex-start;
background-color: #16a085;
}
#chatForm {
display: flex;
gap: 10px;
}
#promptInput {
flex-grow: 1;
padding: 10px;
border: 1px solid #34495e;
border-radius: 5px;
color: inherit;
background-color: #2c3e50;
}
#apiKeyInput {
flex-grow: 1;
padding: 10px;
border: 1px solid #34495e;
border-radius: 5px;
color: inherit;
background-color: #2c3e50;
}
#promptInput:focus {
outline: none;
box-shadow: 0 0 0 2px #007BFF;
}
header {
margin-bottom: 20px;
}
header h1 {
font-size: 1.5rem;
text-align: center;
padding: 0.5em;
background: #2980b9;
border-radius: 5px;
}
#sendPrompt {
padding: 10px 20px;
border: none;
border-radius: 5px;
cursor: pointer;
background-color: #2980b9;
color: #ecf0f1;
}
#modelSelectionContainer {
margin-bottom: 20px;
color: #ecf0f1;
}
#chatHistory, #promptInput, #modelSelect {
border: 2px solid #34495e;
}
#modelSelect {
padding: 10px;
border: 1px solid #34495e;
border-radius: 5px;
background-color: #2c3e50;
color: #ecf0f1;
font-family: Arial, sans-serif;
cursor: pointer;
}
#modelSelect:focus {
outline: none;
border-color: #2980b9;
}
label {
color: #ecf0f1;
margin-right: 10px;
}
#modelSelectionContainer {
display: flex;
align-items: center;
justify-content: start;
gap: 10px;
margin-bottom: 10px;
}
pre {
background-color: #333;
color: #f8f8f2;
border: 1px solid #2980b9;
border-left: 3px solid #2980b9;
padding: 10px;
overflow-x: auto;
font-family: 'Courier New', Courier, monospace;
margin: 10px 0;
border-radius: 4px;
white-space: pre-wrap;
}
code {
font-family: 'Courier New', Courier, monospace;
}
::-webkit-scrollbar {
width: 12px;
}
::-webkit-scrollbar-track {
background: #2c3e50;
border-radius: 10px;
}
::-webkit-scrollbar-thumb {
background: #34495e;
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
background: #2980b9;
}
@keyframes ellipsis {
0%, 20% {
content: '';
}
40% {
content: '.';
}
60% {
content: '..';
}
80%, 100% {
content: '...';
}
}
.ellipsis::after {
content: '';
animation: ellipsis 2s infinite;
}

32
gui/html/index.html Normal file
View File

@ -0,0 +1,32 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Kuzco Chat</title>
<link rel="stylesheet" href="../css/styles.css">
</head>
<body>
<div id="app">
<header>
<h1>Welcome to Kuzco Chat</h1>
</header>
<div id="modelSelectionContainer">
<label for="modelSelect">Choose AI Model:</label>
<select id="modelSelect">
<option value="mistral">Mistral</option>
<option value="llama2">Llama2</option>
</select>
</div>
<main id="chatHistory" class="chat-history">
</main>
<footer>
<form id="chatForm" class="chat-form">
<input id="promptInput" type="text" placeholder="Enter your prompt" autofocus>
<button id="sendButton">Send</button>
<button id="stopButton" disabled>Stop</button>
</form>
</footer>
</div>
<script src="../js/renderer.js"></script>
</body>
</html>

20
gui/html/prompt.html Normal file
View File

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html>
<head>
<title>Enter API Key</title>
<link rel="stylesheet" href="../css/styles.css">
</head>
<body>
<h1>Enter API Key</h1>
<input type="text" id="apiKeyInput" placeholder="API Key">
<button id="submitApiKey">Submit</button>
<script>
const { ipcRenderer } = require('electron');
document.getElementById('submitApiKey').addEventListener('click', () => {
const apiKey = document.getElementById('apiKeyInput').value;
ipcRenderer.send('submit-api-key', apiKey);
});
</script>
</body>
</html>

92
gui/js/kuzcoCore.js Normal file
View File

@ -0,0 +1,92 @@
const fs = require('fs');
const path = require('path');
const os = require('os');
const fetch = require('node-fetch');
async function fetchData(url, options = {}) {
const { default: fetch } = await import('node-fetch');
const response = await fetch(url, options);
return response;
}
class KuzcoCore {
constructor() {
this.configPath = path.join(os.homedir(), '.kuzco-cli', 'config.json');
this.API_KEY = this.loadApiKey();
this.controller = new AbortController();
this.isAborted = false;
}
loadApiKey() {
try {
if (fs.existsSync(this.configPath)) {
const configFile = fs.readFileSync(this.configPath);
const config = JSON.parse(configFile);
return config.API_KEY;
} else {
console.log('API Key config file does not exist. Please set up your API Key.');
return '';
}
} catch (error) {
console.error(`An error occurred while reading the API key: ${error.message}`);
return '';
}
}
apiKeyExists() {
return fs.existsSync(this.configPath) && this.API_KEY !== '';
}
abortFetch() {
this.isAborted = true;
this.controller.abort();
}
async sendPrompt(prompt, model) {
console.log("Model received in sendPrompt:", model)
this.controller = new AbortController();
const signal = this.controller.signal;
const timeoutId = setTimeout(() => this.controller.abort(), 25000);
try {
const response = await fetch('https://relay.kuzco.xyz/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
messages: [{ role: 'user', 'content': prompt + '\n' }],
model: model,
stream: false,
}),
signal: signal,
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
clearTimeout(timeoutId);
const errorMessage = this.isAborted
? 'Request aborted by the user. Please try again.'
: 'Request timed out. Please try again.';
this.isAborted = false;
this.controller = new AbortController();
return error.name === 'AbortError'
? { error: errorMessage }
: { error: error.message };
}
}
}
module.exports = KuzcoCore;

7
gui/js/preload.js Normal file
View File

@ -0,0 +1,7 @@
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', {
sendPrompt: (prompt, model) => ipcRenderer.invoke('send-prompt', { prompt, model }),
onApiKeySaved: (callback) => ipcRenderer.on('api-key-saved', callback),
abortPrompt: () => ipcRenderer.send('abort-prompt')
});

99
gui/js/renderer.js Normal file
View File

@ -0,0 +1,99 @@
function displayMessage(message, sender) {
const chatHistory = document.getElementById('chatHistory');
const messageDiv = document.createElement('div');
messageDiv.classList.add('message');
const parts = message.split('```');
for (let i = 0; i < parts.length; i++) {
if (i % 2 === 0) {
const textPart = document.createElement('span');
textPart.textContent = parts[i];
messageDiv.appendChild(textPart);
} else {
const codeBlock = document.createElement('pre');
const code = document.createElement('code');
code.textContent = parts[i];
codeBlock.appendChild(code);
messageDiv.appendChild(codeBlock);
}
}
if (sender === 'user') {
messageDiv.classList.add('userMessage');
} else {
messageDiv.classList.add('assistantMessage');
}
chatHistory.appendChild(messageDiv);
chatHistory.scrollTop = chatHistory.scrollHeight;
}
document.addEventListener('DOMContentLoaded', () => {
const chatForm = document.getElementById('chatForm');
const promptInput = document.getElementById('promptInput');
const modelSelect = document.getElementById('modelSelect');
const sendButton = document.getElementById('sendButton');
const stopButton = document.getElementById('stopButton');
const modelSelectionContainer = document.getElementById('modelSelectionContainer');
if (chatForm && promptInput && modelSelect) {
chatForm.addEventListener('submit', async (event) => {
event.preventDefault();
const userInput = promptInput.value;
const selectedModel = modelSelect.value;
promptInput.value = '';
modelSelectionContainer.style.display = 'none';
sendButton.disabled = true;
promptInput.disabled = true;
stopButton.disabled = false;
displayMessage(userInput, 'user');
const typingIndicator = displayTypingIndicator();
try {
const response = await window.electronAPI.sendPrompt(userInput, selectedModel);
if (response.error) {
throw new Error(response.error);
}
const assistantMessage = response.choices[0].message.content.trim();
displayMessage(assistantMessage, 'assistant');
} catch (error) {
console.error(`Error sending prompt: ${error.message}`);
displayMessage(`Error: ${error.message}`, 'assistant');
} finally {
typingIndicator.remove();
sendButton.disabled = false;
promptInput.disabled = false;
promptInput.focus();
stopButton.disabled = true;
}
});
} else {
console.error('chatForm or promptInput elements not found!');
}
});
document.getElementById('stopButton').addEventListener('click', () => {
window.electronAPI.abortPrompt();
document.getElementById('stopButton').disabled = true;
});
function displayTypingIndicator() {
const chatHistory = document.getElementById('chatHistory');
const typingIndicator = document.createElement('div');
typingIndicator.classList.add('message', 'assistantMessage', 'ellipsis');
typingIndicator.textContent = 'Processing';
chatHistory.appendChild(typingIndicator);
chatHistory.scrollTop = chatHistory.scrollHeight;
return typingIndicator;
}
document.addEventListener('keydown', (e) => {
if (e.key === 'F11') {
e.preventDefault();
ipcRenderer.send('toggle-fullscreen');
}
});

82
gui/kuzco-gui.js Normal file
View File

@ -0,0 +1,82 @@
const { app, BrowserWindow, ipcMain, dialog } = require('electron');
const fs = require('fs');
const path = require('path');
const os = require('os');
const KuzcoCore = require('./js/kuzcoCore');
function createWindow() {
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'js/preload.js'),
nodeIntegration: false,
contextIsolation: true,
enableRemoteModule: false,
},
});
mainWindow.loadFile('html/index.html');
if (!kuzcoCore.apiKeyExists()) {
promptForApiKey(mainWindow);
}
}
ipcMain.on('submit-api-key', (event, apiKey) => {
const configDir = path.join(os.homedir(), '.kuzco-cli');
const configPath = path.join(configDir, 'config.json');
if (!fs.existsSync(configDir)){
fs.mkdirSync(configDir, { recursive: true });
}
fs.writeFileSync(configPath, JSON.stringify({ API_KEY: apiKey }, null, 2), 'utf8');
event.reply('api-key-saved');
app.relaunch();
app.quit();
});
let inputWindow;
function promptForApiKey() {
inputWindow = new BrowserWindow({
width: 300,
height: 300,
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
enableRemoteModule: true,
},
});
inputWindow.loadFile('html/prompt.html');
inputWindow.on('closed', () => {
inputWindow = null;
});
}
app.whenReady().then(() => {
createWindow();
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow();
});
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit();
});
const kuzcoCore = new KuzcoCore();
ipcMain.handle('send-prompt', async (event, { prompt, model }) => {
console.log("Received model in main process:", model);
return await kuzcoCore.sendPrompt(prompt, model);
});
ipcMain.on('abort-prompt', () => {
kuzcoCore.abortFetch();
});

3506
gui/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

62
gui/package.json Normal file
View File

@ -0,0 +1,62 @@
{
"name": "kuzco-gui",
"version": "0.0.1",
"description": "Simple gui for kuzco api",
"author": "Wizzard <Wizzard@deadzone.lol>",
"main": "kuzco-gui.js",
"homepage": "https://git.deadzone.lol/Wizzard/kuzco-cli",
"scripts": {
"start": "electron .",
"pack": "electron-builder --dir",
"dist-linux": "electron-builder --linux",
"dist-mac": "electron-builder --mac",
"dist-win": "electron-builder --win",
"dist-all": "electron-builder -mwl"
},
"build": {
"appId": "com.codjointops.kuzco",
"productName": "KuzcoChat",
"directories": {
"output": "dist"
},
"files": [
"**/*",
"css/**/*",
"js/**/*",
"html/**/*",
"!**/*.ts",
"!*.code-workspace",
"!**/*.js.map",
"!**/node_modules/*/{CHANGELOG.md,README.md,README,readme.md,readme}",
"!**/node_modules/*/{test,__tests__,tests,powered-test,example,examples}",
"!**/node_modules/*.d.ts",
"!**/node_modules/.bin",
"!**/*.{o,hprof,orig,pyc,pyo,rbc}",
"!**/._*",
"!**/{.DS_Store,.git,.hg,.svn,CVS,RCS,SCCS,__pycache__,thumbs.db,.db,desktop.ini}"
],
"win": {
"target": [
"nsis",
"portable"
]
},
"mac": {
"target": "dmg",
"category": "public.app-category.utilities"
},
"linux": {
"target": [
"AppImage",
"deb"
]
}
},
"devDependencies": {
"electron": "latest",
"electron-builder": "^22.0.0"
},
"dependencies": {
"node-fetch": "^2.7.0"
}
}