diff --git a/src/cli.ts b/src/cli.ts index 5ae3cfab..f0316739 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -493,15 +493,21 @@ program .command('web') .description('Start the web interface') .option('-p, --port ', 'Port to listen on', '3000') + .option('--https', 'Enable HTTPS with a self-signed certificate (enables browser Notifications API)') .action(async (options) => { const { startWebServer } = await import('./web/server.js'); const port = parseInt(options.port, 10); + const https = !!options.https; + const protocol = https ? 'https' : 'http'; - console.log(chalk.cyan(`Starting Claudeman web interface on port ${port}...`)); + console.log(chalk.cyan(`Starting Claudeman web interface on port ${port}${https ? ' (HTTPS)' : ''}...`)); try { - await startWebServer(port); - console.log(chalk.green(`\nāœ“ Web interface running at http://localhost:${port}`)); + await startWebServer(port, https); + console.log(chalk.green(`\nāœ“ Web interface running at ${protocol}://localhost:${port}`)); + if (https) { + console.log(chalk.yellow(' Note: Accept the self-signed certificate in your browser on first visit')); + } console.log(chalk.gray(' Press Ctrl+C to stop\n')); } catch (err) { console.error(chalk.red(`āœ— Failed to start web server: ${getErrorMessage(err)}`)); diff --git a/src/web/server.ts b/src/web/server.ts index 859b225a..89c2f22e 100644 --- a/src/web/server.ts +++ b/src/web/server.ts @@ -15,6 +15,7 @@ import fastifyStatic from '@fastify/static'; import { join, dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import { existsSync, mkdirSync, writeFileSync, readdirSync, readFileSync } from 'node:fs'; +import { execSync } from 'node:child_process'; import { homedir, totalmem, freemem, loadavg, cpus } from 'node:os'; import { EventEmitter } from 'node:events'; import { Session, ClaudeMessage, type BackgroundTask, type RalphTrackerState, type RalphTodoItem } from '../session.js'; @@ -129,6 +130,39 @@ function autoConfigureRalph(session: Session, workingDir: string, broadcast: (ev } } +/** + * Get or generate a self-signed TLS certificate for HTTPS. + * Certs are stored in ~/.claudeman/certs/ and reused across restarts. + */ +function getOrCreateSelfSignedCert(): { key: string; cert: string } { + const certsDir = join(homedir(), '.claudeman', 'certs'); + const keyPath = join(certsDir, 'server.key'); + const certPath = join(certsDir, 'server.crt'); + + if (existsSync(keyPath) && existsSync(certPath)) { + return { + key: readFileSync(keyPath, 'utf-8'), + cert: readFileSync(certPath, 'utf-8'), + }; + } + + mkdirSync(certsDir, { recursive: true }); + + // Generate self-signed cert valid for 365 days, covering localhost and common LAN access patterns + execSync( + `openssl req -x509 -newkey rsa:2048 -nodes ` + + `-keyout "${keyPath}" -out "${certPath}" ` + + `-days 365 -subj "/CN=claudeman" ` + + `-addext "subjectAltName=DNS:localhost,IP:127.0.0.1,IP:0.0.0.0"`, + { stdio: 'pipe' } + ); + + return { + key: readFileSync(keyPath, 'utf-8'), + cert: readFileSync(certPath, 'utf-8'), + }; +} + export class WebServer extends EventEmitter { private app: FastifyInstance; private sessions: Map = new Map(); @@ -138,6 +172,7 @@ export class WebServer extends EventEmitter { private sseClients: Set = new Set(); private store = getStore(); private port: number; + private https: boolean; private screenManager: ScreenManager; // Terminal batching for performance private terminalBatches: Map = new Map(); @@ -159,10 +194,17 @@ export class WebServer extends EventEmitter { // Spawn1337 agent orchestrator private spawnOrchestrator: SpawnOrchestrator; - constructor(port: number = 3000) { + constructor(port: number = 3000, https: boolean = false) { super(); this.port = port; - this.app = Fastify({ logger: false }); + this.https = https; + + if (https) { + const { key, cert } = getOrCreateSelfSignedCert(); + this.app = Fastify({ logger: false, https: { key, cert } }); + } else { + this.app = Fastify({ logger: false }); + } this.screenManager = new ScreenManager(); // Set up screen manager event listeners @@ -2157,10 +2199,11 @@ export class WebServer extends EventEmitter { async start(): Promise { await this.setupRoutes(); await this.app.listen({ port: this.port, host: '0.0.0.0' }); - console.log(`Claudeman web interface running at http://localhost:${this.port}`); + const protocol = this.https ? 'https' : 'http'; + console.log(`Claudeman web interface running at ${protocol}://localhost:${this.port}`); // Set API URL for child processes (MCP server, spawned sessions) - process.env.CLAUDEMAN_API_URL = `http://localhost:${this.port}`; + process.env.CLAUDEMAN_API_URL = `${protocol}://localhost:${this.port}`; // Start scheduled runs cleanup timer this.scheduledCleanupTimer = setInterval(() => { @@ -2405,8 +2448,8 @@ export class WebServer extends EventEmitter { } } -export async function startWebServer(port: number = 3000): Promise { - const server = new WebServer(port); +export async function startWebServer(port: number = 3000, https: boolean = false): Promise { + const server = new WebServer(port, https); await server.start(); return server; }