* fix: specify TS_NODE_PROJECT for dev:server script ts-node/register resolves tsconfig from the entry file upward, finding the root solution-style tsconfig.json (no compilerOptions). This causes target to default to ES3, breaking MapIterator spread syntax (TS2802). Set TS_NODE_PROJECT env var to point to the server tsconfig which targets ES2024. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add token usage tracking, context display, and dynamic context length - Intercept SSE proxy to capture run.completed events and persist token usage (input_tokens, output_tokens) per session to SQLite/JSON store - Display context usage bar in ChatInput showing used/total/remaining tokens - Resolve actual context length from Hermes models_dev_cache.json based on the active profile's default model (fallback 200K), with 5min in-memory cache - Move sessions-db.ts to db/hermes/ for unified database layer - Add usage store with SQLite + JSON fallback (auto-migration via ensureTable) - Fix proxy SSE path regex to match rewritten upstream path - Fix route ordering: /sessions/usage before /sessions/:id to avoid 404 - Fetch per-session usage on session enter instead of batch - Add unit tests for usage-store, db index, and proxy SSE interception Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
108 lines
3.6 KiB
TypeScript
108 lines
3.6 KiB
TypeScript
import Koa from 'koa'
|
|
import cors from '@koa/cors'
|
|
import bodyParser from '@koa/bodyparser'
|
|
import serve from 'koa-static'
|
|
import send from 'koa-send'
|
|
import os from 'os'
|
|
import { resolve } from 'path'
|
|
import { mkdir } from 'fs/promises'
|
|
import { readFileSync } from 'fs'
|
|
import { config } from './config'
|
|
import { getToken, requireAuth } from './services/auth'
|
|
import { initGatewayManager } from './services/gateway-bootstrap'
|
|
import { bindShutdown } from './services/shutdown'
|
|
import { setupTerminalWebSocket } from './routes/hermes/terminal'
|
|
import { startVersionCheck } from './routes/health'
|
|
import { registerRoutes } from './routes'
|
|
import { logger } from './services/logger'
|
|
|
|
// Injected by esbuild at build time; fallback to reading package.json in dev mode
|
|
declare const __APP_VERSION__: string
|
|
const APP_VERSION = typeof __APP_VERSION__ !== 'undefined'
|
|
? __APP_VERSION__
|
|
: (() => { try { return JSON.parse(readFileSync(resolve(__dirname, '../../package.json'), 'utf-8')).version } catch { return 'dev' } })()
|
|
|
|
// Global error handlers
|
|
process.on('uncaughtException', (err) => {
|
|
logger.fatal(err, 'Uncaught exception')
|
|
process.exit(1)
|
|
})
|
|
|
|
process.on('unhandledRejection', (reason) => {
|
|
logger.error(reason, 'Unhandled rejection')
|
|
})
|
|
|
|
let server: any = null
|
|
|
|
export async function bootstrap() {
|
|
console.log(`hermes-web-ui v${APP_VERSION} starting...`)
|
|
await mkdir(config.uploadDir, { recursive: true })
|
|
await mkdir(config.dataDir, { recursive: true })
|
|
|
|
const authToken = await getToken()
|
|
const app = new Koa()
|
|
|
|
await initGatewayManager()
|
|
console.log('[bootstrap] gateway manager initialized')
|
|
|
|
// Initialize web-ui SQLite tables
|
|
const { initUsageStore } = await import('./db/hermes/usage-store')
|
|
initUsageStore()
|
|
console.log('[bootstrap] usage store initialized')
|
|
|
|
app.use(cors({ origin: config.corsOrigins }))
|
|
app.use(bodyParser())
|
|
console.log('[bootstrap] cors + bodyParser registered')
|
|
|
|
// Register all routes (handles auth internally)
|
|
const proxyMiddleware = registerRoutes(app, requireAuth(authToken))
|
|
app.use(proxyMiddleware)
|
|
console.log('[bootstrap] routes registered')
|
|
|
|
if (authToken) {
|
|
console.log(`Auth enabled — token: ${authToken}`)
|
|
logger.info('Auth enabled — token: %s', authToken)
|
|
}
|
|
|
|
// SPA fallback
|
|
const distDir = resolve(__dirname, '..', 'client')
|
|
app.use(serve(distDir))
|
|
app.use(async (ctx) => {
|
|
if (!ctx.path.startsWith('/api') &&
|
|
ctx.path !== '/health' &&
|
|
ctx.path !== '/upload' &&
|
|
ctx.path !== '/webhook') {
|
|
await send(ctx, 'index.html', { root: distDir })
|
|
}
|
|
})
|
|
console.log('[bootstrap] SPA fallback registered')
|
|
|
|
// Start server
|
|
console.log(`[bootstrap] listening on port ${config.port}`)
|
|
server = app.listen(config.port, '0.0.0.0')
|
|
console.log('[bootstrap] app.listen called')
|
|
|
|
setupTerminalWebSocket(server)
|
|
console.log('[bootstrap] terminal websocket setup')
|
|
|
|
server.on('listening', () => {
|
|
const interfaces = os.networkInterfaces()
|
|
const localIp = Object.values(interfaces).flat().find(i => i?.family === 'IPv4' && !i?.internal)?.address || 'localhost'
|
|
console.log(`Server: http://localhost:${config.port} (LAN: http://${localIp}:${config.port})`)
|
|
console.log(`Upstream: ${config.upstream}`)
|
|
console.log(`Log: ~/.hermes-web-ui/logs/server.log`)
|
|
logger.info('Server: http://localhost:%d (LAN: http://%s:%d)', config.port, localIp, config.port)
|
|
logger.info('Upstream: %s', config.upstream)
|
|
})
|
|
|
|
server.on('error', (err: any) => {
|
|
console.error('[bootstrap] server error:', err.code || err.message)
|
|
logger.error({ err }, 'Server error')
|
|
})
|
|
|
|
bindShutdown(server)
|
|
startVersionCheck()
|
|
}
|
|
|
|
bootstrap()
|