Skip to content

Commit 150dd98

Browse files
committed
feat: initialize desktop application with Tauri and Vite
- Added package.json for desktop package with scripts and dependencies. - Created Cargo.toml for Tauri backend with necessary dependencies. - Implemented build.rs for Tauri build process. - Added icons for the application in various sizes and formats. - Developed main.rs with server management commands and chat message handling. - Configured tauri.conf.json for application settings and bundling. - Created style.css for application styling. - Set up Vite configuration for development server and build settings.
1 parent 8ca4678 commit 150dd98

21 files changed

Lines changed: 2320 additions & 69 deletions

packages/cli/src/api/server.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,10 @@ async function handleGenerateOnce(
104104

105105
try {
106106
const geminiClient = config.getGeminiClient();
107+
108+
// Debug: Log available tools
109+
console.error('[API] Available tools:', config.getExcludeTools());
110+
console.error('[API] Approval mode:', config.getApprovalMode());
107111

108112
const { processedQuery, shouldProceed } = await handleAtCommand({
109113
query: input,
@@ -260,6 +264,33 @@ export function startApiServer(
260264
{
261265
status: 'ok',
262266
timestamp: new Date().toISOString(),
267+
mode: 'server',
268+
},
269+
enableCors,
270+
);
271+
}
272+
273+
if (url.pathname === '/status' && req.method === 'GET') {
274+
if (!checkAuth(req, res, authToken, enableCors)) return;
275+
276+
return sendJson(
277+
res,
278+
200,
279+
{
280+
status: 'ready',
281+
timestamp: new Date().toISOString(),
282+
version: '1.0.0', // TODO: Get from package.json
283+
mode: 'server-only',
284+
endpoints: {
285+
generate: '/v1/generate',
286+
health: '/healthz',
287+
status: '/status'
288+
},
289+
features: {
290+
streaming: true,
291+
conversationHistory: true,
292+
toolExecution: true
293+
}
263294
},
264295
enableCors,
265296
);

packages/cli/src/config/config.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,10 @@ export interface CliArgs {
116116
tavilyApiKey: string | undefined;
117117
screenReader: boolean | undefined;
118118
vlmSwitchMode: string | undefined;
119+
serverOnly: boolean | undefined;
120+
noUiOutput: boolean | undefined;
121+
apiPort: number | undefined;
122+
apiHost: string | undefined;
119123
}
120124

121125
export async function parseArguments(settings: Settings): Promise<CliArgs> {
@@ -290,6 +294,26 @@ export async function parseArguments(settings: Settings): Promise<CliArgs> {
290294
'Default behavior when images are detected in input. Values: once (one-time switch), session (switch for entire session), persist (continue with current model). Overrides settings files.',
291295
default: process.env['VLM_SWITCH_MODE'],
292296
})
297+
.option('server-only', {
298+
type: 'boolean',
299+
description: 'Run in server-only mode (no interactive UI). Ideal for desktop app integration.',
300+
default: false,
301+
})
302+
.option('no_ui_output', {
303+
type: 'boolean',
304+
description: 'Disable all UI output and run silently. Use with --server-only for headless operation.',
305+
default: false,
306+
})
307+
.option('api-port', {
308+
type: 'number',
309+
description: 'Port for the API server to listen on.',
310+
default: process.env['KOLOSAL_CLI_API_PORT'] ? Number(process.env['KOLOSAL_CLI_API_PORT']) : undefined,
311+
})
312+
.option('api-host', {
313+
type: 'string',
314+
description: 'Host for the API server to bind to.',
315+
default: process.env['KOLOSAL_CLI_API_HOST'],
316+
})
293317
.check((argv) => {
294318
if (argv.prompt && argv['promptInteractive']) {
295319
throw new Error(
@@ -495,8 +519,9 @@ export async function loadCliConfig(
495519
const interactive =
496520
!!argv.promptInteractive || (process.stdin.isTTY && question.length === 0);
497521
// In non-interactive mode, exclude tools that require a prompt.
522+
// However, in server-only mode, we want to allow all tools for API access.
498523
const extraExcludes: string[] = [];
499-
if (!interactive && !argv.experimentalAcp) {
524+
if (!interactive && !argv.experimentalAcp && !argv.serverOnly) {
500525
switch (approvalMode) {
501526
case ApprovalMode.PLAN:
502527
case ApprovalMode.DEFAULT:

packages/cli/src/gemini.tsx

Lines changed: 146 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { startServerIfEnabled, stopGlobalServer } from './server/kolosal-server-
88

99
import type { Config } from '@kolosal-code/kolosal-code-core';
1010
import {
11+
ApprovalMode,
1112
AuthType,
1213
FatalConfigError,
1314
getOauthClient,
@@ -229,6 +230,134 @@ export async function startInteractiveUI(
229230
registerCleanup(() => instance.unmount());
230231
}
231232

233+
export async function startServerOnly(
234+
config: Config,
235+
settings: LoadedSettings,
236+
workspaceRoot: string,
237+
): Promise<void> {
238+
// Server-only mode - no UI, no interactive elements
239+
// Skip UI initialization, theme loading, and desktop integration
240+
// But keep essential authentication and client initialization
241+
242+
// CRITICAL: Initialize the config - this sets up contentGeneratorConfig
243+
await config.initialize();
244+
245+
// Get authType from current model (needed for client initialization)
246+
const { getCurrentModelAuthType, getSavedModelEntry } = await import('./config/savedModels.js');
247+
const currentModelName = settings.merged.model?.name;
248+
const savedModels = (settings.merged.model?.savedModels ?? []) as SavedModelEntry[];
249+
const currentAuthType = getCurrentModelAuthType(currentModelName, savedModels);
250+
const currentModelEntry = getSavedModelEntry(currentModelName, savedModels);
251+
const hasStoredApiKey = Boolean(currentModelEntry?.apiKey?.trim());
252+
const hasPersistedKolosalToken = Boolean(
253+
typeof settings.merged.kolosalOAuthToken === 'string' &&
254+
settings.merged.kolosalOAuthToken.trim(),
255+
);
256+
const usesOpenAICompatibleProvider = currentModelEntry?.provider === 'openai-compatible';
257+
258+
// Set approval mode to YOLO before creating the client to ensure all tools are available
259+
const originalApprovalMode = config.getApprovalMode();
260+
config.setApprovalMode(ApprovalMode.YOLO);
261+
262+
// CRITICAL: Create the Gemini client by calling refreshAuth
263+
// This is what actually creates this.geminiClient
264+
try {
265+
await config.refreshAuth(currentAuthType || AuthType.NO_AUTH);
266+
} catch (err) {
267+
if (config.getDebugMode()) {
268+
console.error('[server-only] Client initialization failed:', err);
269+
}
270+
// Continue anyway - some operations might work without auth
271+
}
272+
273+
// Restore original approval mode after client creation
274+
config.setApprovalMode(originalApprovalMode);
275+
276+
// Handle additional authentication if needed
277+
const shouldPreAuthenticate =
278+
currentAuthType === AuthType.USE_OPENAI &&
279+
config.isBrowserLaunchSuppressed() &&
280+
!hasStoredApiKey &&
281+
!hasPersistedKolosalToken &&
282+
!usesOpenAICompatibleProvider;
283+
284+
if (shouldPreAuthenticate) {
285+
try {
286+
await getOauthClient(currentAuthType, config);
287+
} catch (err) {
288+
if (config.getDebugMode()) {
289+
console.error('[server-only] Authentication failed:', err);
290+
}
291+
// Continue anyway - some operations might work without auth
292+
}
293+
}
294+
295+
// Start kolosal-server in the background if enabled
296+
const serverManager = await startServerIfEnabled({
297+
debug: config.getDebugMode(),
298+
autoStart: true,
299+
port: 8087,
300+
});
301+
302+
// Register cleanup to stop the server when CLI exits
303+
if (serverManager) {
304+
registerCleanup(async () => {
305+
try {
306+
await stopGlobalServer();
307+
} catch (error) {
308+
if (config.getDebugMode()) {
309+
console.error('Error stopping kolosal-server:', error);
310+
}
311+
}
312+
});
313+
}
314+
315+
// Start API server with forced enabled state for server-only mode
316+
try {
317+
const { startApiServer } = await import('./api/server.js');
318+
const port = Number(process.env['KOLOSAL_CLI_API_PORT'] ?? settings.merged.api?.port ?? 38080);
319+
const host = process.env['KOLOSAL_CLI_API_HOST'] ?? settings.merged.api?.host ?? '127.0.0.1';
320+
const authToken = process.env['KOLOSAL_CLI_API_TOKEN'] ?? settings.merged.api?.token;
321+
322+
const apiServer = await startApiServer(config, {
323+
port,
324+
host,
325+
enableCors: true, // Always enable CORS for desktop app
326+
authToken,
327+
});
328+
329+
registerCleanup(async () => {
330+
try { await apiServer.close(); } catch { /* ignore */ }
331+
});
332+
333+
// Always log debug info in server-only mode for troubleshooting
334+
console.error(`[server-only] API server listening on http://${host}:${port}`);
335+
console.error(`[server-only] Model: ${currentModelName}, Auth: ${currentAuthType}`);
336+
console.error(`[server-only] Excluded tools:`, config.getExcludeTools());
337+
} catch (e) {
338+
console.error('Failed to start API server in server-only mode:', e);
339+
throw e;
340+
}
341+
342+
// Keep the process alive and handle graceful shutdown
343+
const shutdown = async () => {
344+
if (config.getDebugMode()) {
345+
console.error('[server-only] Shutting down...');
346+
}
347+
await runExitCleanup();
348+
process.exit(0);
349+
};
350+
351+
process.once('SIGINT', shutdown);
352+
process.once('SIGTERM', shutdown);
353+
process.once('SIGUSR2', shutdown); // For nodemon
354+
355+
// Log startup completion only in debug mode
356+
if (config.getDebugMode()) {
357+
console.error(`[server-only] Kolosal CLI server ready on port ${process.env['KOLOSAL_CLI_API_PORT'] ?? 38080}`);
358+
}
359+
}
360+
232361
export async function main() {
233362
setupProcessExitHandlers();
234363
setupUnhandledRejectionHandler();
@@ -289,6 +418,21 @@ export async function main() {
289418

290419
setMaxSizedBoxDebugging(config.getDebugMode());
291420

421+
// Check for server-only mode first (before config.initialize())
422+
if (argv.serverOnly) {
423+
// Override API settings for server-only mode
424+
process.env['KOLOSAL_CLI_API'] = 'true'; // Force API enabled
425+
if (argv.apiPort) {
426+
process.env['KOLOSAL_CLI_API_PORT'] = argv.apiPort.toString();
427+
}
428+
if (argv.apiHost) {
429+
process.env['KOLOSAL_CLI_API_HOST'] = argv.apiHost;
430+
}
431+
432+
await startServerOnly(config, settings, workspaceRoot);
433+
return; // Exit early for server-only mode
434+
}
435+
292436
await config.initialize();
293437

294438
// Optionally start lightweight HTTP API server to expose generation endpoints
@@ -301,8 +445,8 @@ export async function main() {
301445
if (apiEnabled) {
302446
try {
303447
const { startApiServer } = await import('./api/server.js');
304-
const port = Number(process.env['KOLOSAL_CLI_API_PORT'] ?? settings.merged.api?.port ?? 38080);
305-
const host = process.env['KOLOSAL_CLI_API_HOST'] ?? settings.merged.api?.host ?? '127.0.0.1';
448+
const port = Number(argv.apiPort ?? process.env['KOLOSAL_CLI_API_PORT'] ?? settings.merged.api?.port ?? 38080);
449+
const host = argv.apiHost ?? process.env['KOLOSAL_CLI_API_HOST'] ?? settings.merged.api?.host ?? '127.0.0.1';
306450
const authToken = process.env['KOLOSAL_CLI_API_TOKEN'] ?? settings.merged.api?.token;
307451
const corsEnabled = (process.env['KOLOSAL_CLI_API_CORS'] ?? '')
308452
? ['1','true','yes'].includes(String(process.env['KOLOSAL_CLI_API_CORS']).toLowerCase())

0 commit comments

Comments
 (0)