File size: 3,313 Bytes
e92be04
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import express from "express";
import cors from "cors";
import net from "net";
import path from "path";
import { fileURLToPath } from "url";
import fs from "fs";

async function findAvailablePort(startPort) {
  return new Promise((resolve) => {
    const probe = net.createServer();
    probe.listen(startPort, () => {
      probe.close(() => resolve(startPort));
    });
    probe.on("error", () => resolve(findAvailablePort(startPort + 1)));
  });
}

export function setupServer(app) {
  const __filename = fileURLToPath(import.meta.url);
  const __dirname = path.dirname(__filename);

  app.use(cors());
  app.use(express.json());

  // Global Error Handling
  process.on('uncaughtException', (err) => {
    console.error('CRITICAL: Uncaught Exception:', err);
  });

  process.on('unhandledRejection', (reason, promise) => {
    console.error('CRITICAL: Unhandled Rejection:', reason);
  });

  // Serve static backups
  const BACKUP_SERVE_DIR = path.join(__dirname, '../../../../public', 'backups'); // Adjust path as per new monorepo structure
  if (!fs.existsSync(BACKUP_SERVE_DIR)) {
    fs.mkdirSync(BACKUP_SERVE_DIR, { recursive: true });
    console.log('[Archivist] Created backup directory:', BACKUP_SERVE_DIR);
  }
  app.use('/backups', express.static(BACKUP_SERVE_DIR));

  // Serve the frontend app
  const APP_FRONTEND_DIR = path.join(__dirname, '../../../app');
  // NOTE: Static file serving is intentionally NOT registered here.
  // index.js registers app.use(express.static(APP_DIR)) AFTER all API routes,
  // which gives routes (including /silicon with content-negotiation) priority
  // over static files. Registering static BEFORE routes would intercept /silicon
  // and always return HTML regardless of the Accept header.

  // Markdown for Agents Middleware
  app.use((req, res, next) => {
    req.prefersMarkdown = req.headers['accept']?.includes('text/markdown');
    res.setHeader("X-Agent-Friendly", "true");
    res.setHeader("X-Hive-Status", "active");
    res.setHeader("X-Agent-Reward", "available");
    if (req.headers['user-agent']?.toLowerCase().includes('bot') || req.headers['user-agent']?.toLowerCase().includes('agent')) {
      res.setHeader("X-Treasure-Path", "/agent-welcome.json");
    }
    next();
  });

  // Agent-First header
  app.use((req, res, next) => {
    res.setHeader('X-Agent-View', 'https://openclaw-agent-01-production-63d8.up.railway.app/agent-view');
    next();
  });

  return app;
}

export async function startServer(app, preferredPort = 3000) {
  const port = preferredPort; // Skip probing - trust the environment/config
  return new Promise((resolve, reject) => {
    const httpServer = app.listen(port, "0.0.0.0", () => {
      console.log(`P2PCLAW Gateway running on 0.0.0.0:${port}`);
      resolve({ port, httpServer });
    }).on("error", (err) => {
      console.error(`[Server] Failed to bind to port ${port}:`, err.message);
      reject(err);
    });
  });
}

// Helper to serve markdown
export function serveMarkdown(res, markdown) {
  const estimateTokens = (text) => Math.ceil((text || "").length / 4);
  const tokens = estimateTokens(markdown);
  res.setHeader("Content-Type", "text/markdown; charset=utf-8");
  res.setHeader("x-markdown-tokens", tokens.toString());
  res.setHeader("Vary", "Accept");
  res.send(markdown);
}