lemonade-interactive-loader 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -83,16 +83,17 @@ The main menu adapts based on whether you have a configuration saved:
83
83
 
84
84
  ### The Setup Wizard
85
85
 
86
- Just answer 8 simple questions:
86
+ Just answer 9 simple questions:
87
87
 
88
88
  1. **Network access?** Should the server be accessible from other devices?
89
89
  2. **Port number?** Which port should it run on? (default: 8080)
90
90
  3. **Logging level?** Choose from info, debug, warning, or error
91
- 4. **Model directory?** Point to existing models (like LM Studio) if needed
92
- 5. **Interface type?** System tray or headless mode
93
- 6. **Custom arguments?** Any additional llama.cpp parameters?
94
- 7. **Custom build?** Use a specific llama.cpp build from GitHub?
95
- 8. **Backend?** Choose auto, vulkan, rocm, or cpu
91
+ 4. **Context window size?** Choose from 4K, 8K, 16K, 32K, 64K, 128K, or 256K tokens (default: 4K)
92
+ 5. **Model directory?** Point to existing models (like LM Studio) if needed
93
+ 6. **Interface type?** System tray or headless mode
94
+ 7. **Custom arguments?** Any additional llama.cpp parameters?
95
+ 8. **Custom build?** Use a specific llama.cpp build from GitHub?
96
+ 9. **Backend?** Choose auto, vulkan, rocm, or cpu
96
97
 
97
98
  ## ✨ Key Features
98
99
 
@@ -169,7 +170,7 @@ Point Lemonade Server to your existing model directory (like LM Studio's):
169
170
 
170
171
  Configuration is automatically saved and loaded:
171
172
 
172
- - **Location**: `~/.lemonade-launcher/config.json` (Linux/macOS) or `%USERPROFILE%\.lemonade-launcher\config.json` (Windows)
173
+ - **Location**: `~/.lemonade-interactive-launcher/config.json` (Linux/macOS) or `%USERPROFILE%\.lemonade-interactive-launcher\config.json` (Windows)
173
174
  - **Format**: JSON
174
175
  - **Auto-saved**: After every setup or edit
175
176
 
@@ -250,7 +251,7 @@ Run Command Prompt or PowerShell as Administrator.
250
251
 
251
252
  #### Build download fails
252
253
  - Check your internet connection
253
- - Ensure you have write permissions to `~/.lemonade-launcher/`
254
+ - Ensure you have write permissions to `~/.lemonade-interactive-launcher/`
254
255
  - Try downloading the asset manually from GitHub
255
256
 
256
257
  #### Server won't start
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lemonade-interactive-loader",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "Interactive CLI tool to launch Lemonade Server with custom arguments and download llama.cpp releases - Cross-platform (Windows/Linux)",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -35,6 +35,7 @@
35
35
  "adm-zip": "^0.5.16",
36
36
  "inquirer": "^8.2.6",
37
37
  "tar": "^7.5.9",
38
+ "tree-kill": "^1.2.2",
38
39
  "unzipper": "^0.12.3"
39
40
  }
40
41
  }
package/src/README.md CHANGED
@@ -43,7 +43,7 @@ src/
43
43
  ### CLI (`cli/`)
44
44
  - **menu.js**: Main menu system and command routing
45
45
  - **prompts.js**: Interactive prompts for release/asset selection
46
- - **setup-wizard.js**: 8-question setup wizard
46
+ - **setup-wizard.js**: 9-question setup wizard
47
47
 
48
48
  ## Architecture Principles
49
49
 
package/src/cli/menu.js CHANGED
@@ -213,6 +213,7 @@ async function viewConfiguration() {
213
213
  console.log(`Host: ${config.host}`);
214
214
  console.log(`Port: ${config.port}`);
215
215
  console.log(`Log Level: ${config.logLevel}`);
216
+ console.log(`Context Size: ${config.contextSize}`);
216
217
  console.log(`Backend: ${config.backend}`);
217
218
  console.log(`Model Directory: ${config.modelDir}`);
218
219
  console.log(`Run Mode: ${config.runMode}`);
@@ -263,15 +264,20 @@ async function resetConfiguration() {
263
264
  /**
264
265
  * Handle main menu command
265
266
  * @param {string} command - Selected command
267
+ * @returns {Promise<boolean>} True if should exit the app
266
268
  */
267
269
  async function handleCommand(command) {
270
+ let shouldExit = false;
271
+
268
272
  switch (command) {
269
273
  case 'setup':
270
274
  await runSetupWizard(false);
271
275
  if (await askLaunchServer()) {
272
276
  const config = loadConfig();
273
277
  if (Object.keys(config).length > 0) {
278
+ setupShutdownHandlers();
274
279
  await launchLemonadeServer(config);
280
+ shouldExit = true;
275
281
  }
276
282
  }
277
283
  break;
@@ -281,7 +287,9 @@ async function handleCommand(command) {
281
287
  if (await askLaunchServer()) {
282
288
  const config = loadConfig();
283
289
  if (Object.keys(config).length > 0) {
290
+ setupShutdownHandlers();
284
291
  await launchLemonadeServer(config);
292
+ shouldExit = true;
285
293
  }
286
294
  }
287
295
  break;
@@ -294,7 +302,7 @@ async function handleCommand(command) {
294
302
  await resetConfiguration();
295
303
  break;
296
304
 
297
-
305
+
298
306
 
299
307
  case 'manage':
300
308
  let manageAction;
@@ -319,13 +327,17 @@ async function handleCommand(command) {
319
327
  const config = loadConfig();
320
328
  if (Object.keys(config).length === 0) {
321
329
  console.log('No configuration found. Please run "setup" first.');
322
- return;
330
+ return false;
323
331
  }
324
332
  // Set up shutdown handlers to kill server on exit
325
333
  setupShutdownHandlers();
326
334
  await launchLemonadeServer(config);
335
+ // After server exits, exit the app completely
336
+ shouldExit = true;
327
337
  break;
328
338
  }
339
+
340
+ return shouldExit;
329
341
  }
330
342
 
331
343
  /**
@@ -336,7 +348,12 @@ async function runCLI() {
336
348
 
337
349
  while (continueRunning) {
338
350
  const command = await showMainMenu();
339
- await handleCommand(command);
351
+ const shouldExit = await handleCommand(command);
352
+
353
+ // Exit the app if we just exited from the server
354
+ if (shouldExit) {
355
+ return;
356
+ }
340
357
 
341
358
  const { continueRunning: shouldContinue } = await inquirer.prompt([
342
359
  {
@@ -223,6 +223,7 @@ function displayConfigSummary(config) {
223
223
  console.log(`Host: ${config.host}`);
224
224
  console.log(`Port: ${config.port}`);
225
225
  console.log(`Log Level: ${config.logLevel}`);
226
+ console.log(`Context Size: ${config.contextSize}`);
226
227
  console.log(`Backend: ${config.backend}`);
227
228
  console.log(`Model Directory: ${config.modelDir}`);
228
229
  console.log(`Run Mode: ${config.runMode}`);
@@ -1,5 +1,5 @@
1
1
  const inquirer = require('inquirer');
2
- const { DEFAULTS, BACKEND_TYPES, LOG_LEVELS, RUN_MODES, HOSTS } = require('../config/constants');
2
+ const { DEFAULTS, BACKEND_TYPES, LOG_LEVELS, RUN_MODES, HOSTS, CONTEXT_SIZES } = require('../config/constants');
3
3
  const { loadConfig, saveConfig } = require('../config');
4
4
  const { selectLlamaCppRelease, selectAsset, selectInstalledAssetPrompt, displayConfigSummary } = require('./prompts');
5
5
  const { downloadAndExtractLlamaCpp } = require('../services/asset-manager');
@@ -54,16 +54,42 @@ async function runSetupWizard(isEdit = false) {
54
54
  }
55
55
  ]);
56
56
 
57
- // Q4: Custom model directory
57
+ // Q4: Context window size
58
+ const savedContextSize = existingConfig.contextSize || DEFAULTS.CONTEXT_SIZE;
59
+ const contextSizeChoices = [
60
+ { name: '4K (4096 tokens) - Default', value: CONTEXT_SIZES['4K'] },
61
+ { name: '8K (8192 tokens)', value: CONTEXT_SIZES['8K'] },
62
+ { name: '16K (16384 tokens)', value: CONTEXT_SIZES['16K'] },
63
+ { name: '32K (32768 tokens)', value: CONTEXT_SIZES['32K'] },
64
+ { name: '64K (65536 tokens)', value: CONTEXT_SIZES['64K'] },
65
+ { name: '128K (131072 tokens)', value: CONTEXT_SIZES['128K'] },
66
+ { name: '256K (262144 tokens)', value: CONTEXT_SIZES['256K'] }
67
+ ];
68
+
69
+ // Find the index of the saved/default context size
70
+ const defaultContextSizeIndex = contextSizeChoices.findIndex(choice => choice.value === savedContextSize);
71
+ const defaultContextSize = defaultContextSizeIndex >= 0 ? defaultContextSizeIndex : 0;
72
+
73
+ const { contextSize } = await inquirer.prompt([
74
+ {
75
+ type: 'list',
76
+ name: 'contextSize',
77
+ message: 'How big should the context window be?',
78
+ choices: contextSizeChoices,
79
+ default: defaultContextSize
80
+ }
81
+ ]);
82
+
83
+ // Q5: Custom model directory
58
84
  const existingModelDir = existingConfig.modelDir;
59
- const hasExistingModelDir = existingModelDir && existingModelDir !== 'None';
85
+ const hasExistingModelDir = existingModelDir !== undefined;
60
86
 
61
87
  const { useCustomModelDir } = await inquirer.prompt([
62
88
  {
63
89
  type: 'confirm',
64
90
  name: 'useCustomModelDir',
65
91
  message: 'Is there another model directory to use? (example: LM Studio)',
66
- default: false
92
+ default: hasExistingModelDir
67
93
  }
68
94
  ]);
69
95
 
@@ -83,7 +109,7 @@ async function runSetupWizard(isEdit = false) {
83
109
  finalModelDir = DEFAULTS.MODEL_DIR;
84
110
  }
85
111
 
86
- // Q5: System tray vs headless
112
+ // Q6: System tray vs headless
87
113
  const { runMode } = await inquirer.prompt([
88
114
  {
89
115
  type: 'list',
@@ -97,7 +123,7 @@ async function runSetupWizard(isEdit = false) {
97
123
  }
98
124
  ]);
99
125
 
100
- // Q6: Custom llama.cpp args
126
+ // Q7: Custom llama.cpp args
101
127
  const existingLlamacppArgs = existingConfig.llamacppArgs || '';
102
128
  const hasExistingArgs = existingLlamacppArgs.length > 0;
103
129
 
@@ -117,7 +143,7 @@ async function runSetupWizard(isEdit = false) {
117
143
  {
118
144
  type: 'input',
119
145
  name: 'llamacppArgs',
120
- message: 'Enter llama.cpp arguments (comma-separated, e.g., --ctx-size 4096,--batch-size 512):',
146
+ message: 'Enter llama.cpp arguments (comma-separated, e.g., --no-mmap,--batch-size 512):',
121
147
  default: existingLlamacppArgs
122
148
  }
123
149
  ]);
@@ -126,7 +152,7 @@ async function runSetupWizard(isEdit = false) {
126
152
  finalLlamacppArgs = '';
127
153
  }
128
154
 
129
- // Q7: Custom llama.cpp build
155
+ // Q8: Custom llama.cpp build
130
156
  const existingCustomPath = existingConfig.customLlamacppPath || '';
131
157
  const hasExistingBuild = existingCustomPath.length > 0;
132
158
 
@@ -209,6 +235,7 @@ async function runSetupWizard(isEdit = false) {
209
235
  host,
210
236
  port: parseInt(port),
211
237
  logLevel,
238
+ contextSize,
212
239
  backend,
213
240
  modelDir: finalModelDir,
214
241
  runMode,
@@ -2,14 +2,14 @@ const path = require('path');
2
2
  const os = require('os');
3
3
 
4
4
  // Configuration directories
5
- const USER_CONFIG_DIR = path.join(os.homedir(), '.lemonade-launcher');
5
+ const USER_CONFIG_DIR = path.join(os.homedir(), '.lemonade-interactive-launcher');
6
6
  const USER_CONFIG_FILE = path.join(USER_CONFIG_DIR, 'config.json');
7
- const DEFAULT_LLAMACPP_INSTALL_DIR = path.join(os.homedir(), '.lemonade-launcher', 'llama-cpp');
7
+ const DEFAULT_LLAMACPP_INSTALL_DIR = path.join(os.homedir(), '.lemonade-interactive-launcher', 'llama-cpp');
8
8
 
9
9
  // GitHub API
10
10
  const GITHUB_RELEASES_URL = 'https://api.github.com/repos/ggml-org/llama.cpp/releases';
11
11
  const GITHUB_API_HEADERS = {
12
- 'User-Agent': 'lemonade-launcher',
12
+ 'User-Agent': 'lemonade-interactive-launcher',
13
13
  'Accept': 'application/vnd.github.v3+json'
14
14
  };
15
15
 
@@ -57,6 +57,17 @@ const HOSTS = {
57
57
  ALL_INTERFACES: '0.0.0.0'
58
58
  };
59
59
 
60
+ // Context window sizes (in tokens)
61
+ const CONTEXT_SIZES = {
62
+ '4K': 4096,
63
+ '8K': 8192,
64
+ '16K': 16384,
65
+ '32K': 32768,
66
+ '64K': 65536,
67
+ '128K': 131072,
68
+ '256K': 262144
69
+ };
70
+
60
71
  // Default values
61
72
  const DEFAULTS = {
62
73
  PORT: 8080,
@@ -65,7 +76,8 @@ const DEFAULTS = {
65
76
  EXPOSE_TO_NETWORK: false,
66
77
  RUN_MODE: RUN_MODES.SYSTEM_TRAY,
67
78
  MODEL_DIR: 'None',
68
- BACKEND: BACKEND_TYPES.AUTO
79
+ BACKEND: BACKEND_TYPES.AUTO,
80
+ CONTEXT_SIZE: CONTEXT_SIZES['4K']
69
81
  };
70
82
 
71
83
  module.exports = {
@@ -79,5 +91,6 @@ module.exports = {
79
91
  LOG_LEVELS,
80
92
  RUN_MODES,
81
93
  HOSTS,
94
+ CONTEXT_SIZES,
82
95
  DEFAULTS
83
96
  };
@@ -20,7 +20,7 @@ function downloadFile(url, outputPath) {
20
20
 
21
21
  const file = fs.createWriteStream(outputPath);
22
22
 
23
- const req = protocol.get(url, { headers: { 'User-Agent': 'lemonade-launcher' } }, (res) => {
23
+ const req = protocol.get(url, { headers: { 'User-Agent': 'lemonade-interactive-launcher' } }, (res) => {
24
24
  if (res.statusCode === 302 || res.statusCode === 301) {
25
25
  downloadFile(res.headers.location, outputPath)
26
26
  .then(resolve)
@@ -1,11 +1,14 @@
1
1
  const fs = require('fs');
2
2
  const { execSync, spawn } = require('child_process');
3
+ const kill = require('tree-kill');
3
4
  const { LEMONADE_SERVER_DEFAULT_PATH } = require('../config/constants');
4
5
  const { findLlamaServer } = require('../utils/system');
5
6
  const { getLlamaServerPath } = require('./asset-manager');
6
7
 
7
8
  // Track the server process for graceful shutdown
8
9
  let serverProcess = null;
10
+ let isShuttingDown = false;
11
+ let hasExited = false;
9
12
 
10
13
  /**
11
14
  * Build server command arguments
@@ -13,7 +16,7 @@ let serverProcess = null;
13
16
  * @returns {Array} Array of command arguments
14
17
  */
15
18
  function buildServerArgs(config) {
16
- const { host, port, logLevel, modelDir, llamacppArgs } = config;
19
+ const { host, port, logLevel, modelDir, llamacppArgs, contextSize } = config;
17
20
  const args = [
18
21
  'serve',
19
22
  '--log-level', logLevel || 'info',
@@ -21,6 +24,10 @@ function buildServerArgs(config) {
21
24
  '--port', port.toString()
22
25
  ];
23
26
 
27
+ if (contextSize) {
28
+ args.push('--ctx-size', contextSize.toString());
29
+ }
30
+
24
31
  if (modelDir && modelDir !== 'None') {
25
32
  args.push('--extra-models-dir', modelDir);
26
33
  }
@@ -76,6 +83,7 @@ async function launchLemonadeServer(config) {
76
83
  host,
77
84
  port,
78
85
  logLevel,
86
+ contextSize,
79
87
  modelDir,
80
88
  llamacppArgs,
81
89
  runMode,
@@ -89,6 +97,7 @@ async function launchLemonadeServer(config) {
89
97
  console.log(`Host: ${host}`);
90
98
  console.log(`Port: ${port}`);
91
99
  console.log(`Log Level: ${logLevel}`);
100
+ console.log(`Context Size: ${contextSize || 'default'}`);
92
101
  console.log(`Backend: ${backend || 'auto'}`);
93
102
  console.log(`Model Directory: ${modelDir || 'default'}`);
94
103
  console.log(`Run Mode: ${runMode || 'headless'}`);
@@ -157,25 +166,38 @@ async function launchLemonadeServer(config) {
157
166
  console.log('\nStarting server...\n');
158
167
 
159
168
  try {
169
+ // Build the command for spawning
170
+ const commandStr = formatCommand(serverPath, args, {});
171
+
160
172
  // Parse the command into executable and arguments
161
- const parts = command.trim().split(/\s+/);
173
+ const parts = commandStr.trim().split(/\s+/);
162
174
  const executable = parts[0];
163
175
  const execArgs = parts.slice(1);
164
176
 
177
+ console.log(`Spawning: ${executable} ${execArgs.join(' ')}`);
178
+
165
179
  // Spawn the server process
166
180
  serverProcess = spawn(executable, execArgs, {
167
181
  stdio: 'inherit',
168
182
  env: process.env
169
183
  });
170
184
 
171
- // Handle process exit
172
- serverProcess.on('close', (code) => {
173
- console.log(`\nServer exited with code ${code}`);
174
- serverProcess = null;
175
- });
176
-
177
- serverProcess.on('error', (err) => {
178
- console.error(`Server process error: ${err.message}`);
185
+ // Wait for the process to exit (blocking)
186
+ await new Promise((resolve, reject) => {
187
+ serverProcess.on('exit', (code, signal) => {
188
+ // Only log if shutdown wasn't initiated by us
189
+ if (!hasExited) {
190
+ console.log(`\nServer exited with status ${code || 'None'} and signal ${signal || 'None'}`);
191
+ }
192
+ serverProcess = null;
193
+ resolve(code);
194
+ });
195
+
196
+ serverProcess.on('error', (err) => {
197
+ console.error(`Server process error: ${err.message}`);
198
+ serverProcess = null;
199
+ reject(err);
200
+ });
179
201
  });
180
202
 
181
203
  } catch (error) {
@@ -187,37 +209,95 @@ async function launchLemonadeServer(config) {
187
209
  }
188
210
 
189
211
  /**
190
- * Gracefully shutdown the lemonade server
212
+ * Gracefully shutdown the lemonade server and all child processes
191
213
  */
192
214
  function shutdownLemonadeServer() {
193
- if (serverProcess) {
194
- console.log('\n\nShutting down Lemonade Server...');
195
-
196
- // Try graceful shutdown first (SIGTERM)
197
- serverProcess.on('exit', () => {
198
- console.log('Server shut down successfully.');
199
- });
200
-
201
- serverProcess.on('error', (err) => {
202
- console.error(`Error shutting down server: ${err.message}`);
203
- });
204
-
205
- // Send SIGTERM signal
206
- serverProcess.kill('SIGTERM');
207
-
208
- // Force kill after 5 seconds if still running
209
- setTimeout(() => {
210
- if (serverProcess && !serverProcess.killed) {
211
- console.log('Force killing server...');
212
- serverProcess.kill('SIGKILL');
213
- }
214
- }, 5000);
215
+ // Prevent duplicate shutdown calls
216
+ if (isShuttingDown) {
217
+ console.log('Shutdown already in progress...');
218
+ return;
219
+ }
220
+
221
+ if (!serverProcess || !serverProcess.pid) {
222
+ console.log('No server process to shut down.');
223
+ return;
215
224
  }
225
+
226
+ isShuttingDown = true;
227
+ hasExited = false;
228
+ console.log('\n\nShutting down Lemonade Server and child processes...');
229
+
230
+ // Check if process is still running
231
+ try {
232
+ // Sending signal 0 checks if process exists without actually sending a signal
233
+ process.kill(serverProcess.pid, 0);
234
+ } catch (err) {
235
+ // Process is already dead
236
+ console.log('Server process is already terminated.');
237
+ serverProcess = null;
238
+ isShuttingDown = false;
239
+ return;
240
+ }
241
+
242
+ // Remove existing event listeners to prevent duplicates
243
+ serverProcess.removeAllListeners('exit');
244
+ serverProcess.removeAllListeners('error');
245
+
246
+ // Try graceful shutdown first (SIGINT)
247
+ serverProcess.on('exit', (code, signal) => {
248
+ hasExited = true;
249
+ console.log(`Server exited with status ${code || 'None'} and signal ${signal || 'None'}`);
250
+ console.log('Server shut down successfully.');
251
+ serverProcess = null;
252
+ isShuttingDown = false;
253
+ });
254
+
255
+ serverProcess.on('error', (err) => {
256
+ console.error(`Error shutting down server: ${err.message}`);
257
+ });
258
+
259
+ // Use tree-kill to terminate the process and all its children
260
+ kill(serverProcess.pid, 'SIGINT', (err) => {
261
+ if (err && !hasExited) {
262
+ // Only log error if process hasn't already exited naturally
263
+ if (err.code !== 'ESRCH') {
264
+ console.log(`Note: Could not kill process tree: ${err.message}`);
265
+ }
266
+ }
267
+ });
268
+
269
+ // Force kill with SIGKILL after 3 seconds if still running
270
+ setTimeout(() => {
271
+ // Don't force kill if already exited
272
+ if (hasExited || !isShuttingDown) return;
273
+
274
+ // Check again if process is still running before force kill
275
+ try {
276
+ process.kill(serverProcess.pid, 0);
277
+ // Process still exists, force kill it
278
+ kill(serverProcess.pid, 'SIGKILL', (err) => {
279
+ if (err && !hasExited) {
280
+ // Only log error if process hasn't already exited naturally
281
+ if (err.code !== 'ESRCH') {
282
+ console.log(`Note: Could not force kill process: ${err.message}`);
283
+ }
284
+ }
285
+ });
286
+ } catch (err) {
287
+ // Process has already exited
288
+ console.log('Process has already exited.');
289
+ }
290
+ }, 3000);
216
291
  }
217
292
 
218
293
  // Set up signal handlers for graceful shutdown
219
294
  function setupShutdownHandlers() {
220
295
  const shutdown = (signal) => {
296
+ if (isShuttingDown) {
297
+ console.log(`Received ${signal}, but shutdown already in progress...`);
298
+ return;
299
+ }
300
+
221
301
  console.log(`\n\nReceived ${signal}. Shutting down...`);
222
302
  shutdownLemonadeServer();
223
303
  setTimeout(() => process.exit(0), 2000);