All files server.ts

44.23% Statements 23/52
70% Branches 7/10
61.53% Functions 8/13
43.13% Lines 22/51

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112                                                                    3x 3x       3x 3x 3x 3x       3x 3x       7x       7x 7x 7x   7x             7x 7x 7x                                       1x 1x       1x 1x       2x       7x                  
import chalk from 'chalk';
import express, { type Request, type Response } from 'express';
import logSymbols from 'log-symbols';
import { ClaudeBrowser } from './browser.js';
import { logger, ts } from './logger.js';
import type { BrowserCommand, BrowserOptions } from './types.js';
 
export interface ServerOptions extends BrowserOptions {
  port?: number;
}
 
function printBanner(port: number): void {
  console.log();
  console.log(chalk.cyan.bold('  🌐 Claude Browse Server'));
  console.log(chalk.dim('  ─────────────────────────'));
  console.log(`  ${logSymbols.success} Listening on ${chalk.bold(`http://localhost:${port}`)}`);
  console.log();
  console.log(chalk.dim('  Commands:'));
  console.log(
    `    ${chalk.cyan('goto')}  ${chalk.yellow('click')}  ${chalk.magenta('type')}  ${chalk.blue('query')}  ${chalk.green('screenshot')}`
  );
  console.log(
    `    ${chalk.cyan('url')}  ${chalk.blue('html')}  ${chalk.yellow('back')}  ${chalk.yellow('forward')}  ${chalk.yellow('reload')}  ${chalk.gray('wait')}  ${chalk.red('close')}`
  );
  console.log();
  console.log(chalk.dim('  Example:'));
  console.log(
    chalk.gray(`    curl -X POST localhost:${port} -d '{"cmd":"goto","url":"https://example.com"}'`)
  );
  console.log();
}
 
export class BrowserServer {
  private browser: ClaudeBrowser;
  private app = express();
  private server: ReturnType<typeof this.app.listen> | null = null;
  private port: number;
 
  constructor(options: ServerOptions = {}) {
    this.browser = new ClaudeBrowser(options);
    this.port = options.port ?? 13373;
    this.setupMiddleware();
    this.setupRoutes();
  }
 
  private setupMiddleware(): void {
    this.app.use(express.json());
    this.app.use(express.text({ type: '*/*' }));
  }
 
  private setupRoutes(): void {
    this.app.post('/', (req, res) => this.handleCommand(req, res));
  }
 
  private async handleCommand(req: Request, res: Response): Promise<void> {
    try {
      const cmd: BrowserCommand = typeof req.body === 'string' ? JSON.parse(req.body) : req.body;
      logger.command(cmd);
 
      Iif (cmd.cmd === 'close') {
        logger.result(cmd, { ok: true });
        res.json({ ok: true });
        await this.stop();
        process.exit(0);
      }
 
      const result = await this.browser.executeCommand(cmd);
      logger.result(cmd, result);
      res.json(result);
    } catch (err) {
      const error = (err as Error).message;
      console.log(`${ts()}   ${logSymbols.error} ${chalk.red(error)}`);
      res.status(500).json({ ok: false, error });
    }
  }
 
  async start(): Promise<void> {
    await this.browser.launch();
 
    return new Promise((resolve) => {
      this.server = this.app.listen(this.port, () => {
        printBanner(this.port);
        resolve();
      });
    });
  }
 
  async stop(): Promise<void> {
    console.log(chalk.dim('\n  Shutting down...'));
    Iif (this.server) {
      this.server.close();
      this.server = null;
    }
    await this.browser.close();
    console.log(`  ${logSymbols.success} Browser closed\n`);
  }
 
  getPort(): number {
    return this.port;
  }
 
  getApp() {
    return this.app;
  }
}
 
export async function startServer(options: ServerOptions = {}): Promise<BrowserServer> {
  const server = new BrowserServer(options);
  await server.start();
  return server;
}