refactor: rename configuration variables for clarity

Standardize naming across CLI and server components:
- CLI: serverUrl → apiUrl, HAPI_SERVER_URL → HAPI_API_URL
- Server: webapp* → listen*, miniAppUrl → publicUrl, WEBAPP_* → HAPI_LISTEN_*, WEBAPP_URL → HAPI_PUBLIC_URL
- Rename serverUrlInit.ts → apiUrlInit.ts with updated logic for backward compatibility
- Update all imports, function calls, and documentation accordingly
This commit is contained in:
weishu
2026-01-19 12:57:37 +08:00
parent 0228146b99
commit 9e335fa305
27 changed files with 176 additions and 140 deletions
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
run: | run: |
{ {
echo "HAPI_HOME=~/.hapi-dev-test" echo "HAPI_HOME=~/.hapi-dev-test"
echo "HAPI_SERVER_URL=http://localhost:3006" echo "HAPI_API_URL=http://localhost:3006"
echo "CLI_API_TOKEN=${CLI_API_TOKEN:-dev-test-token}" echo "CLI_API_TOKEN=${CLI_API_TOKEN:-dev-test-token}"
echo "HAPI_DAEMON_HTTP_TIMEOUT=60000" echo "HAPI_DAEMON_HTTP_TIMEOUT=60000"
echo "HAPI_DAEMON_HEARTBEAT_INTERVAL=30000" echo "HAPI_DAEMON_HEARTBEAT_INTERVAL=30000"
+1 -1
View File
@@ -139,7 +139,7 @@ User interface components.
hapi runner start hapi runner start
# With custom bot URL (for local development): # With custom bot URL (for local development):
HAPI_SERVER_URL=http://localhost:3006 CLI_API_TOKEN=your_token hapi runner start HAPI_API_URL=http://localhost:3006 CLI_API_TOKEN=your_token hapi runner start
# Stop the runner: # Stop the runner:
hapi runner stop hapi runner stop
+1 -1
View File
@@ -67,7 +67,7 @@ See `src/configuration.ts` for all options.
### Required ### Required
- `CLI_API_TOKEN` - Shared secret; must match the server. Can be set via env or `~/.hapi/settings.json` (env wins). - `CLI_API_TOKEN` - Shared secret; must match the server. Can be set via env or `~/.hapi/settings.json` (env wins).
- `HAPI_SERVER_URL` - Server base URL (default: http://localhost:3006). - `HAPI_API_URL` - Server base URL (default: http://localhost:3006).
### Optional ### Optional
+2 -2
View File
@@ -19,7 +19,7 @@ export class ApiClient {
state: AgentState | null state: AgentState | null
}): Promise<Session> { }): Promise<Session> {
const response = await axios.post<CreateSessionResponse>( const response = await axios.post<CreateSessionResponse>(
`${configuration.serverUrl}/cli/sessions`, `${configuration.apiUrl}/cli/sessions`,
{ {
tag: opts.tag, tag: opts.tag,
metadata: opts.metadata, metadata: opts.metadata,
@@ -79,7 +79,7 @@ export class ApiClient {
runnerState?: RunnerState runnerState?: RunnerState
}): Promise<Machine> { }): Promise<Machine> {
const response = await axios.post<CreateMachineResponse>( const response = await axios.post<CreateMachineResponse>(
`${configuration.serverUrl}/cli/machines`, `${configuration.apiUrl}/cli/machines`,
{ {
id: opts.machineId, id: opts.machineId,
metadata: opts.metadata, metadata: opts.metadata,
+1 -1
View File
@@ -215,7 +215,7 @@ export class ApiMachineClient {
} }
connect(): void { connect(): void {
this.socket = io(`${configuration.serverUrl}/cli`, { this.socket = io(`${configuration.apiUrl}/cli`, {
transports: ['websocket'], transports: ['websocket'],
auth: { auth: {
token: this.token, token: this.token,
+2 -2
View File
@@ -71,7 +71,7 @@ export class ApiSessionClient extends EventEmitter {
registerCommonHandlers(this.rpcHandlerManager, this.metadata.path) registerCommonHandlers(this.rpcHandlerManager, this.metadata.path)
} }
this.socket = io(`${configuration.serverUrl}/cli`, { this.socket = io(`${configuration.apiUrl}/cli`, {
auth: { auth: {
token: this.token, token: this.token,
clientType: 'session-scoped' as const, clientType: 'session-scoped' as const,
@@ -270,7 +270,7 @@ export class ApiSessionClient extends EventEmitter {
let cursor = startSeq let cursor = startSeq
while (true) { while (true) {
const response = await axios.get( const response = await axios.get(
`${configuration.serverUrl}/cli/sessions/${encodeURIComponent(this.sessionId)}/messages`, `${configuration.apiUrl}/cli/sessions/${encodeURIComponent(this.sessionId)}/messages`,
{ {
params: { afterSeq: cursor, limit }, params: { afterSeq: cursor, limit },
headers: { headers: {
+1 -1
View File
@@ -21,7 +21,7 @@ export async function handleAuthCommand(args: string[]): Promise<void> {
const hasToken = Boolean(envToken || settingsToken) const hasToken = Boolean(envToken || settingsToken)
const tokenSource = envToken ? 'environment' : (settingsToken ? 'settings file' : 'none') const tokenSource = envToken ? 'environment' : (settingsToken ? 'settings file' : 'none')
console.log(chalk.bold('\nDirect Connect Status\n')) console.log(chalk.bold('\nDirect Connect Status\n'))
console.log(chalk.gray(` HAPI_SERVER_URL: ${configuration.serverUrl}`)) console.log(chalk.gray(` HAPI_API_URL: ${configuration.apiUrl}`))
console.log(chalk.gray(` CLI_API_TOKEN: ${hasToken ? 'set' : 'missing'}`)) console.log(chalk.gray(` CLI_API_TOKEN: ${hasToken ? 'set' : 'missing'}`))
console.log(chalk.gray(` Token Source: ${tokenSource}`)) console.log(chalk.gray(` Token Source: ${tokenSource}`))
console.log(chalk.gray(` Machine ID: ${settings.machineId ?? 'not set'}`)) console.log(chalk.gray(` Machine ID: ${settings.machineId ?? 'not set'}`))
+1 -1
View File
@@ -139,7 +139,7 @@ ${chalk.bold.cyan('Claude Code Options (from `claude --help`):')}
messageLower.includes('network error') messageLower.includes('network error')
) { ) {
console.error(chalk.yellow('Unable to connect to HAPI server')) console.error(chalk.yellow('Unable to connect to HAPI server'))
console.error(chalk.gray(` Server URL: ${configuration.serverUrl}`)) console.error(chalk.gray(` Server URL: ${configuration.apiUrl}`))
console.error(chalk.gray(' Please check your network connection or server status')) console.error(chalk.gray(' Please check your network connection or server status'))
} else if (httpStatus === 403 && responseErrorText === 'Machine access denied') { } else if (httpStatus === 403 && responseErrorText === 'Machine access denied') {
console.error(chalk.red('Machine access denied.')) console.error(chalk.red('Machine access denied.'))
+6 -6
View File
@@ -12,7 +12,7 @@ import packageJson from '../package.json'
import { getCliArgs } from '@/utils/cliArgs' import { getCliArgs } from '@/utils/cliArgs'
class Configuration { class Configuration {
private _serverUrl: string private _apiUrl: string
private _cliApiToken: string private _cliApiToken: string
public readonly isRunnerProcess: boolean public readonly isRunnerProcess: boolean
@@ -29,7 +29,7 @@ class Configuration {
constructor() { constructor() {
// Server configuration // Server configuration
this._serverUrl = process.env.HAPI_SERVER_URL || 'http://localhost:3006' this._apiUrl = process.env.HAPI_API_URL || 'http://localhost:3006'
this._cliApiToken = process.env.CLI_API_TOKEN || '' this._cliApiToken = process.env.CLI_API_TOKEN || ''
// Check if we're running as runner based on process args // Check if we're running as runner based on process args
@@ -64,12 +64,12 @@ class Configuration {
} }
} }
get serverUrl(): string { get apiUrl(): string {
return this._serverUrl return this._apiUrl
} }
_setServerUrl(url: string): void { _setApiUrl(url: string): void {
this._serverUrl = url this._apiUrl = url
} }
get cliApiToken(): string { get cliApiToken(): string {
+3 -1
View File
@@ -17,7 +17,9 @@ interface Settings {
machineIdConfirmedByServer?: boolean machineIdConfirmedByServer?: boolean
runnerAutoStartWhenRunningHappy?: boolean runnerAutoStartWhenRunningHappy?: boolean
cliApiToken?: string cliApiToken?: string
// Server URL for API connections (priority: env HAPI_SERVER_URL > this > default) // API URL for server connections (priority: env HAPI_API_URL > this > default)
apiUrl?: string
// Legacy field name (for migration, read-only)
serverUrl?: string serverUrl?: string
} }
+2 -2
View File
@@ -11,7 +11,7 @@
* *
* The integration test environment uses .env.integration-test which sets: * The integration test environment uses .env.integration-test which sets:
* - HAPI_HOME=~/.hapi-dev-test (DIFFERENT from dev's ~/.hapi-dev!) * - HAPI_HOME=~/.hapi-dev-test (DIFFERENT from dev's ~/.hapi-dev!)
* - HAPI_SERVER_URL=http://localhost:3006 (local hapi-server) * - HAPI_API_URL=http://localhost:3006 (local hapi-server)
* - CLI_API_TOKEN=... (must match the server) * - CLI_API_TOKEN=... (must match the server)
*/ */
@@ -56,7 +56,7 @@ async function isServerHealthy(): Promise<boolean> {
return false; return false;
} }
const url = `${configuration.serverUrl}/cli/machines/__healthcheck__`; const url = `${configuration.apiUrl}/cli/machines/__healthcheck__`;
const response = await fetch(url, { const response = await fetch(url, {
headers: { Authorization: `Bearer ${configuration.cliApiToken}` }, headers: { Authorization: `Bearer ${configuration.cliApiToken}` },
signal: AbortSignal.timeout(1000) signal: AbortSignal.timeout(1000)
+1 -1
View File
@@ -28,7 +28,7 @@ const DEFAULT_IDLE_TIMEOUT_MS = 15 * 60_000
const DEFAULT_MAX_TERMINALS = 4 const DEFAULT_MAX_TERMINALS = 4
const SENSITIVE_ENV_KEYS = new Set([ const SENSITIVE_ENV_KEYS = new Set([
'CLI_API_TOKEN', 'CLI_API_TOKEN',
'HAPI_SERVER_URL', 'HAPI_API_URL',
'HAPI_HTTP_MCP_URL', 'HAPI_HTTP_MCP_URL',
'TELEGRAM_BOT_TOKEN', 'TELEGRAM_BOT_TOKEN',
'OPENAI_API_KEY', 'OPENAI_API_KEY',
@@ -1,7 +1,7 @@
/** /**
* Server URL initialization module * API URL initialization module
* *
* Handles HAPI_SERVER_URL initialization with priority: * Handles HAPI_API_URL initialization with priority:
* 1. Environment variable (highest - allows temporary override) * 1. Environment variable (highest - allows temporary override)
* 2. Settings file (~/.hapi/settings.json) * 2. Settings file (~/.hapi/settings.json)
* 3. Default value (http://localhost:3006) * 3. Default value (http://localhost:3006)
@@ -11,19 +11,24 @@ import { configuration } from '@/configuration'
import { readSettings } from '@/persistence' import { readSettings } from '@/persistence'
/** /**
* Initialize server URL * Initialize API URL
* Must be called before any API operations * Must be called before any API operations
*/ */
export async function initializeServerUrl(): Promise<void> { export async function initializeApiUrl(): Promise<void> {
// 1. Environment variable has highest priority (allows temporary override) // 1. Environment variable has highest priority (allows temporary override)
if (process.env.HAPI_SERVER_URL) { if (process.env.HAPI_API_URL) {
return return
} }
// 2. Read from settings file // 2. Read from settings file (new name first, then legacy)
const settings = await readSettings() const settings = await readSettings()
if (settings.apiUrl) {
configuration._setApiUrl(settings.apiUrl)
return
}
if (settings.serverUrl) { if (settings.serverUrl) {
configuration._setServerUrl(settings.serverUrl) // Migrate from legacy field name
configuration._setApiUrl(settings.serverUrl)
return return
} }
+4 -4
View File
@@ -24,7 +24,7 @@ export function getEnvironmentInfo(): Record<string, any> {
return { return {
PWD: process.env.PWD, PWD: process.env.PWD,
HAPI_HOME: process.env.HAPI_HOME, HAPI_HOME: process.env.HAPI_HOME,
HAPI_SERVER_URL: process.env.HAPI_SERVER_URL, HAPI_API_URL: process.env.HAPI_API_URL,
HAPI_PROJECT_ROOT: process.env.HAPI_PROJECT_ROOT, HAPI_PROJECT_ROOT: process.env.HAPI_PROJECT_ROOT,
CLI_API_TOKEN_SET: Boolean(process.env.CLI_API_TOKEN), CLI_API_TOKEN_SET: Boolean(process.env.CLI_API_TOKEN),
DANGEROUSLY_LOG_TO_SERVER_FOR_AI_AUTO_DEBUGGING: process.env.DANGEROUSLY_LOG_TO_SERVER_FOR_AI_AUTO_DEBUGGING, DANGEROUSLY_LOG_TO_SERVER_FOR_AI_AUTO_DEBUGGING: process.env.DANGEROUSLY_LOG_TO_SERVER_FOR_AI_AUTO_DEBUGGING,
@@ -33,7 +33,7 @@ export function getEnvironmentInfo(): Record<string, any> {
workingDirectory: process.cwd(), workingDirectory: process.cwd(),
processArgv: process.argv, processArgv: process.argv,
happyDir: configuration?.happyHomeDir, happyDir: configuration?.happyHomeDir,
serverUrl: configuration?.serverUrl, apiUrl: configuration?.apiUrl,
logsDir: configuration?.logsDir, logsDir: configuration?.logsDir,
processPid: process.pid, processPid: process.pid,
nodeVersion: process.version, nodeVersion: process.version,
@@ -107,14 +107,14 @@ export async function runDoctorCommand(filter?: 'all' | 'runner'): Promise<void>
// Configuration // Configuration
console.log(chalk.bold('⚙️ Configuration')); console.log(chalk.bold('⚙️ Configuration'));
console.log(`hapi Home: ${chalk.blue(configuration.happyHomeDir)}`); console.log(`hapi Home: ${chalk.blue(configuration.happyHomeDir)}`);
console.log(`Bot URL: ${chalk.blue(configuration.serverUrl)}`); console.log(`Bot URL: ${chalk.blue(configuration.apiUrl)}`);
console.log(`Logs Dir: ${chalk.blue(configuration.logsDir)}`); console.log(`Logs Dir: ${chalk.blue(configuration.logsDir)}`);
// Environment // Environment
console.log(chalk.bold('\n🌍 Environment Variables')); console.log(chalk.bold('\n🌍 Environment Variables'));
const env = getEnvironmentInfo(); const env = getEnvironmentInfo();
console.log(`HAPI_HOME: ${env.HAPI_HOME ? chalk.green(env.HAPI_HOME) : chalk.gray('not set')}`); console.log(`HAPI_HOME: ${env.HAPI_HOME ? chalk.green(env.HAPI_HOME) : chalk.gray('not set')}`);
console.log(`HAPI_SERVER_URL: ${env.HAPI_SERVER_URL ? chalk.green(env.HAPI_SERVER_URL) : chalk.gray('not set')}`); console.log(`HAPI_API_URL: ${env.HAPI_API_URL ? chalk.green(env.HAPI_API_URL) : chalk.gray('not set')}`);
console.log(`CLI_API_TOKEN: ${env.CLI_API_TOKEN_SET ? chalk.green('set') : chalk.gray('not set')}`); console.log(`CLI_API_TOKEN: ${env.CLI_API_TOKEN_SET ? chalk.green('set') : chalk.gray('not set')}`);
console.log(`DANGEROUSLY_LOG_TO_SERVER: ${env.DANGEROUSLY_LOG_TO_SERVER_FOR_AI_AUTO_DEBUGGING ? chalk.yellow('ENABLED') : chalk.gray('not set')}`); console.log(`DANGEROUSLY_LOG_TO_SERVER: ${env.DANGEROUSLY_LOG_TO_SERVER_FOR_AI_AUTO_DEBUGGING ? chalk.yellow('ENABLED') : chalk.gray('not set')}`);
console.log(`DEBUG: ${env.DEBUG ? chalk.green(env.DEBUG) : chalk.gray('not set')}`); console.log(`DEBUG: ${env.DEBUG ? chalk.green(env.DEBUG) : chalk.gray('not set')}`);
+3 -3
View File
@@ -50,10 +50,10 @@ class Logger {
constructor( constructor(
public readonly logFilePath = getSessionLogPath() public readonly logFilePath = getSessionLogPath()
) { ) {
// Remote logging enabled only when explicitly set with server URL // Remote logging enabled only when explicitly set with API URL
if (process.env.DANGEROUSLY_LOG_TO_SERVER_FOR_AI_AUTO_DEBUGGING if (process.env.DANGEROUSLY_LOG_TO_SERVER_FOR_AI_AUTO_DEBUGGING
&& process.env.HAPI_SERVER_URL) { && process.env.HAPI_API_URL) {
this.dangerouslyUnencryptedServerLoggingUrl = process.env.HAPI_SERVER_URL this.dangerouslyUnencryptedServerLoggingUrl = process.env.HAPI_API_URL
console.log(chalk.yellow('[REMOTE LOGGING] Sending logs to server for AI debugging')) console.log(chalk.yellow('[REMOTE LOGGING] Sending logs to server for AI debugging'))
} }
} }
+3 -3
View File
@@ -12,15 +12,15 @@ import { stdin as input, stdout as output } from 'node:process'
import chalk from 'chalk' import chalk from 'chalk'
import { configuration } from '@/configuration' import { configuration } from '@/configuration'
import { readSettings, updateSettings } from '@/persistence' import { readSettings, updateSettings } from '@/persistence'
import { initializeServerUrl } from '@/ui/serverUrlInit' import { initializeApiUrl } from '@/ui/apiUrlInit'
/** /**
* Initialize CLI API token * Initialize CLI API token
* Must be called before any API operations * Must be called before any API operations
*/ */
export async function initializeToken(): Promise<void> { export async function initializeToken(): Promise<void> {
// Initialize server URL first (env > settings.json > default) // Initialize API URL first (env > settings.json > default)
await initializeServerUrl() await initializeApiUrl()
// 1. Environment variable has highest priority (allows temporary override) // 1. Environment variable has highest priority (allows temporary override)
if (configuration.cliApiToken) { if (configuration.cliApiToken) {
+8 -8
View File
@@ -3,7 +3,7 @@
* *
* Automatically starts the HAPI server when CLI is launched * Automatically starts the HAPI server when CLI is launched
* if specific conditions are met: * if specific conditions are met:
* 1. HAPI_SERVER_URL is not set (using default localhost:3006) * 1. HAPI_API_URL is not set (using default localhost:3006)
* 2. cliApiToken exists in settings.json (server was previously started) * 2. cliApiToken exists in settings.json (server was previously started)
* 3. Port 3006 is not currently listening * 3. Port 3006 is not currently listening
*/ */
@@ -90,18 +90,18 @@ async function waitForServerReady(
* Determine if server should be auto-started * Determine if server should be auto-started
*/ */
async function shouldAutoStartServer(): Promise<boolean> { async function shouldAutoStartServer(): Promise<boolean> {
// Condition 1: HAPI_SERVER_URL not set (using default localhost:3006) // Condition 1: HAPI_API_URL not set (using default localhost:3006)
if (process.env.HAPI_SERVER_URL) { if (process.env.HAPI_API_URL) {
logger.debug('[AUTO-START] HAPI_SERVER_URL is set, skipping auto-start') logger.debug('[AUTO-START] HAPI_API_URL is set, skipping auto-start')
return false return false
} }
// Condition 2: Check settings.json // Condition 2: Check settings.json
const settings = await readSettings() const settings = await readSettings()
// 2a: serverUrl is set in settings.json (user configured a specific server) // 2a: apiUrl is set in settings.json (user configured a specific server)
if (settings.serverUrl) { if (settings.apiUrl || settings.serverUrl) {
logger.debug('[AUTO-START] serverUrl is set in settings.json, skipping auto-start') logger.debug('[AUTO-START] apiUrl is set in settings.json, skipping auto-start')
return false return false
} }
@@ -154,7 +154,7 @@ export async function maybeAutoStartServer(): Promise<void> {
startServerAsChild() startServerAsChild()
const isReady = await waitForServerReady(configuration.serverUrl) const isReady = await waitForServerReady(configuration.apiUrl)
if (!isReady) { if (!isReady) {
console.log(chalk.yellow('Warning: Server did not start within expected time')) console.log(chalk.yellow('Warning: Server did not start within expected time'))
+1 -1
View File
@@ -115,7 +115,7 @@ Only if they have your access token. For additional security:
- Ensure server is running: `hapi server` - Ensure server is running: `hapi server`
- Check firewall allows port 3006 - Check firewall allows port 3006
- Verify `HAPI_SERVER_URL` is correct - Verify `HAPI_API_URL` is correct
### "Invalid token" error ### "Invalid token" error
+7 -5
View File
@@ -106,8 +106,10 @@ On first run, HAPI:
| Variable | Default | Description | | Variable | Default | Description |
|----------|---------|-------------| |----------|---------|-------------|
| `CLI_API_TOKEN` | Auto-generated | Shared secret for authentication | | `CLI_API_TOKEN` | Auto-generated | Shared secret for authentication |
| `HAPI_SERVER_URL` | `http://localhost:3006` | Server URL for CLI | | `HAPI_API_URL` | `http://localhost:3006` | Server URL for CLI |
| `WEBAPP_PORT` | `3006` | HTTP server port | | `HAPI_LISTEN_HOST` | `127.0.0.1` | HTTP server bind address |
| `HAPI_LISTEN_PORT` | `3006` | HTTP server port |
| `HAPI_PUBLIC_URL` | - | Public URL for external access |
| `HAPI_HOME` | `~/.hapi` | Config directory path | | `HAPI_HOME` | `~/.hapi` | Config directory path |
| `DB_PATH` | `~/.hapi/hapi.db` | Database file path | | `DB_PATH` | `~/.hapi/hapi.db` | Database file path |
| `CORS_ORIGINS` | - | Allowed CORS origins | | `CORS_ORIGINS` | - | Allowed CORS origins |
@@ -118,7 +120,7 @@ On first run, HAPI:
If the server is not on localhost, set these before running `hapi`: If the server is not on localhost, set these before running `hapi`:
```bash ```bash
export HAPI_SERVER_URL="http://your-server:3006" export HAPI_API_URL="http://your-server:3006"
export CLI_API_TOKEN="your-token-here" export CLI_API_TOKEN="your-token-here"
``` ```
@@ -154,7 +156,7 @@ If you prefer not to use the public relay (e.g., for lower latency or self-manag
https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/ https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/
```bash ```bash
export WEBAPP_URL="https://your-tunnel.trycloudflare.com" export HAPI_PUBLIC_URL="https://your-tunnel.trycloudflare.com"
hapi server hapi server
``` ```
</details> </details>
@@ -194,7 +196,7 @@ Enable Telegram notifications and Mini App access:
```bash ```bash
export TELEGRAM_BOT_TOKEN="your-bot-token" export TELEGRAM_BOT_TOKEN="your-bot-token"
export WEBAPP_URL="https://your-public-url" export HAPI_PUBLIC_URL="https://your-public-url"
hapi server hapi server
``` ```
+7 -6
View File
@@ -22,11 +22,12 @@ See `src/configuration.ts` for all options.
### Optional (Telegram) ### Optional (Telegram)
- `TELEGRAM_BOT_TOKEN` - Token from @BotFather. - `TELEGRAM_BOT_TOKEN` - Token from @BotFather.
- `WEBAPP_URL` - Public HTTPS URL for Telegram Mini App access. Also used to derive default CORS origins for the web app. - `HAPI_PUBLIC_URL` - Public HTTPS URL for Telegram Mini App access. Also used to derive default CORS origins for the web app.
### Optional ### Optional
- `WEBAPP_PORT` - HTTP port (default: 3006). - `HAPI_LISTEN_HOST` - HTTP bind address (default: 127.0.0.1).
- `HAPI_LISTEN_PORT` - HTTP port (default: 3006).
- `CORS_ORIGINS` - Comma-separated origins, or `*`. - `CORS_ORIGINS` - Comma-separated origins, or `*`.
- `HAPI_HOME` - Data directory (default: ~/.hapi). - `HAPI_HOME` - Data directory (default: ~/.hapi).
- `DB_PATH` - SQLite database path (default: HAPI_HOME/hapi.db). - `DB_PATH` - SQLite database path (default: HAPI_HOME/hapi.db).
@@ -38,13 +39,13 @@ Binary (single executable):
```bash ```bash
export TELEGRAM_BOT_TOKEN="..." export TELEGRAM_BOT_TOKEN="..."
export CLI_API_TOKEN="shared-secret" export CLI_API_TOKEN="shared-secret"
export WEBAPP_URL="https://your-domain.example" export HAPI_PUBLIC_URL="https://your-domain.example"
hapi server hapi server
``` ```
If you only need web + CLI, you can omit TELEGRAM_BOT_TOKEN. If you only need web + CLI, you can omit TELEGRAM_BOT_TOKEN.
To enable Telegram, set TELEGRAM_BOT_TOKEN and WEBAPP_URL, start the server, open `/app` To enable Telegram, set TELEGRAM_BOT_TOKEN and HAPI_PUBLIC_URL, start the server, open `/app`
in the bot chat, and bind the Mini App with `CLI_API_TOKEN:<namespace>` when prompted. in the bot chat, and bind the Mini App with `CLI_API_TOKEN:<namespace>` when prompted.
From source: From source:
@@ -200,14 +201,14 @@ The server build output is `server/dist/index.js`, and the web assets are in `we
## Networking notes ## Networking notes
- Telegram Mini Apps require HTTPS and a public URL. If the server has no public IP, use Cloudflare Tunnel or Tailscale and set `WEBAPP_URL` to the HTTPS endpoint. - Telegram Mini Apps require HTTPS and a public URL. If the server has no public IP, use Cloudflare Tunnel or Tailscale and set `WEBAPP_URL` to the HTTPS endpoint.
- If the web app is hosted on a different origin, set `CORS_ORIGINS` (or `WEBAPP_URL`) to include that static host origin. - If the web app is hosted on a different origin, set `CORS_ORIGINS` (or `HAPI_PUBLIC_URL`) to include that static host origin.
## Standalone web hosting ## Standalone web hosting
The web UI can be hosted separately from the server (for example on GitHub Pages or Cloudflare Pages): The web UI can be hosted separately from the server (for example on GitHub Pages or Cloudflare Pages):
1. Build and deploy `web/dist` from the repo root. 1. Build and deploy `web/dist` from the repo root.
2. Set `CORS_ORIGINS` (or `WEBAPP_URL`) to the static host origin. 2. Set `CORS_ORIGINS` (or `HAPI_PUBLIC_URL`) to the static host origin.
3. Open the static site, click the Server button on the login screen, and enter the hapi server origin. 3. Open the static site, click the Server button on the login screen, and enter the hapi server origin.
Leaving the server override empty preserves the default same-origin behavior when the server serves the web assets directly. Leaving the server override empty preserves the default same-origin behavior when the server serves the web assets directly.
+67 -46
View File
@@ -13,9 +13,9 @@ import { getSettingsFile, readSettings, writeSettings } from './settings'
export interface ServerSettings { export interface ServerSettings {
telegramBotToken: string | null telegramBotToken: string | null
telegramNotification: boolean telegramNotification: boolean
webappHost: string listenHost: string
webappPort: number listenPort: number
webappUrl: string publicUrl: string
corsOrigins: string[] corsOrigins: string[]
} }
@@ -24,9 +24,9 @@ export interface ServerSettingsResult {
sources: { sources: {
telegramBotToken: 'env' | 'file' | 'default' telegramBotToken: 'env' | 'file' | 'default'
telegramNotification: 'env' | 'file' | 'default' telegramNotification: 'env' | 'file' | 'default'
webappHost: 'env' | 'file' | 'default' listenHost: 'env' | 'file' | 'default'
webappPort: 'env' | 'file' | 'default' listenPort: 'env' | 'file' | 'default'
webappUrl: 'env' | 'file' | 'default' publicUrl: 'env' | 'file' | 'default'
corsOrigins: 'env' | 'file' | 'default' corsOrigins: 'env' | 'file' | 'default'
} }
savedToFile: boolean savedToFile: boolean
@@ -58,11 +58,11 @@ function parseCorsOrigins(str: string): string[] {
} }
/** /**
* Derive CORS origins from webapp URL * Derive CORS origins from public URL
*/ */
function deriveCorsOrigins(webappUrl: string): string[] { function deriveCorsOrigins(publicUrl: string): string[] {
try { try {
return [new URL(webappUrl).origin] return [new URL(publicUrl).origin]
} catch { } catch {
return [] return []
} }
@@ -87,9 +87,9 @@ export async function loadServerSettings(dataDir: string): Promise<ServerSetting
const sources: ServerSettingsResult['sources'] = { const sources: ServerSettingsResult['sources'] = {
telegramBotToken: 'default', telegramBotToken: 'default',
telegramNotification: 'default', telegramNotification: 'default',
webappHost: 'default', listenHost: 'default',
webappPort: 'default', listenPort: 'default',
webappUrl: 'default', publicUrl: 'default',
corsOrigins: 'default', corsOrigins: 'default',
} }
// telegramBotToken: env > file > null // telegramBotToken: env > file > null
@@ -120,53 +120,74 @@ export async function loadServerSettings(dataDir: string): Promise<ServerSetting
sources.telegramNotification = 'file' sources.telegramNotification = 'file'
} }
// webappHost: env > file > 127.0.0.1 // listenHost: env > file (new or old name) > default
let webappHost = '127.0.0.1' let listenHost = '127.0.0.1'
if (process.env.WEBAPP_HOST) { if (process.env.HAPI_LISTEN_HOST) {
webappHost = process.env.WEBAPP_HOST listenHost = process.env.HAPI_LISTEN_HOST
sources.webappHost = 'env' sources.listenHost = 'env'
if (settings.webappHost === undefined) { if (settings.listenHost === undefined) {
settings.webappHost = webappHost settings.listenHost = listenHost
needsSave = true needsSave = true
} }
} else if (settings.listenHost !== undefined) {
listenHost = settings.listenHost
sources.listenHost = 'file'
} else if (settings.webappHost !== undefined) { } else if (settings.webappHost !== undefined) {
webappHost = settings.webappHost // Migrate from old field name
sources.webappHost = 'file' listenHost = settings.webappHost
sources.listenHost = 'file'
settings.listenHost = listenHost
delete settings.webappHost
needsSave = true
} }
// webappPort: env > file > 3006 // listenPort: env > file (new or old name) > default
let webappPort = 3006 let listenPort = 3006
if (process.env.WEBAPP_PORT) { if (process.env.HAPI_LISTEN_PORT) {
const parsed = parseInt(process.env.WEBAPP_PORT, 10) const parsed = parseInt(process.env.HAPI_LISTEN_PORT, 10)
if (!Number.isFinite(parsed) || parsed <= 0) { if (!Number.isFinite(parsed) || parsed <= 0) {
throw new Error('WEBAPP_PORT must be a valid port number') throw new Error('HAPI_LISTEN_PORT must be a valid port number')
} }
webappPort = parsed listenPort = parsed
sources.webappPort = 'env' sources.listenPort = 'env'
if (settings.webappPort === undefined) { if (settings.listenPort === undefined) {
settings.webappPort = webappPort settings.listenPort = listenPort
needsSave = true needsSave = true
} }
} else if (settings.listenPort !== undefined) {
listenPort = settings.listenPort
sources.listenPort = 'file'
} else if (settings.webappPort !== undefined) { } else if (settings.webappPort !== undefined) {
webappPort = settings.webappPort // Migrate from old field name
sources.webappPort = 'file' listenPort = settings.webappPort
sources.listenPort = 'file'
settings.listenPort = listenPort
delete settings.webappPort
needsSave = true
} }
// webappUrl: env > file > http://localhost:{port} // publicUrl: env > file (new or old name) > default
let webappUrl = `http://localhost:${webappPort}` let publicUrl = `http://localhost:${listenPort}`
if (process.env.WEBAPP_URL) { if (process.env.HAPI_PUBLIC_URL) {
webappUrl = process.env.WEBAPP_URL publicUrl = process.env.HAPI_PUBLIC_URL
sources.webappUrl = 'env' sources.publicUrl = 'env'
if (settings.webappUrl === undefined) { if (settings.publicUrl === undefined) {
settings.webappUrl = webappUrl settings.publicUrl = publicUrl
needsSave = true needsSave = true
} }
} else if (settings.publicUrl !== undefined) {
publicUrl = settings.publicUrl
sources.publicUrl = 'file'
} else if (settings.webappUrl !== undefined) { } else if (settings.webappUrl !== undefined) {
webappUrl = settings.webappUrl // Migrate from old field name
sources.webappUrl = 'file' publicUrl = settings.webappUrl
sources.publicUrl = 'file'
settings.publicUrl = publicUrl
delete settings.webappUrl
needsSave = true
} }
// corsOrigins: env > file > derived from webappUrl // corsOrigins: env > file > derived from publicUrl
let corsOrigins: string[] let corsOrigins: string[]
if (process.env.CORS_ORIGINS) { if (process.env.CORS_ORIGINS) {
corsOrigins = parseCorsOrigins(process.env.CORS_ORIGINS) corsOrigins = parseCorsOrigins(process.env.CORS_ORIGINS)
@@ -179,7 +200,7 @@ export async function loadServerSettings(dataDir: string): Promise<ServerSetting
corsOrigins = settings.corsOrigins corsOrigins = settings.corsOrigins
sources.corsOrigins = 'file' sources.corsOrigins = 'file'
} else { } else {
corsOrigins = deriveCorsOrigins(webappUrl) corsOrigins = deriveCorsOrigins(publicUrl)
} }
// Save settings if any new values were added // Save settings if any new values were added
@@ -191,9 +212,9 @@ export async function loadServerSettings(dataDir: string): Promise<ServerSetting
settings: { settings: {
telegramBotToken, telegramBotToken,
telegramNotification, telegramNotification,
webappHost, listenHost,
webappPort, listenPort,
webappUrl, publicUrl,
corsOrigins, corsOrigins,
}, },
sources, sources,
+5 -1
View File
@@ -14,10 +14,14 @@ export interface Settings {
// Server configuration (persisted from environment variables) // Server configuration (persisted from environment variables)
telegramBotToken?: string telegramBotToken?: string
telegramNotification?: boolean telegramNotification?: boolean
listenHost?: string
listenPort?: number
publicUrl?: string
corsOrigins?: string[]
// Legacy field names (for migration, read-only)
webappHost?: string webappHost?: string
webappPort?: number webappPort?: number
webappUrl?: string webappUrl?: string
corsOrigins?: string[]
} }
export function getSettingsFile(dataDir: string): string { export function getSettingsFile(dataDir: string): string {
+15 -14
View File
@@ -9,8 +9,9 @@
* - CLI_API_TOKEN: Shared secret for hapi CLI authentication (auto-generated if not set) * - CLI_API_TOKEN: Shared secret for hapi CLI authentication (auto-generated if not set)
* - TELEGRAM_BOT_TOKEN: Telegram Bot API token from @BotFather * - TELEGRAM_BOT_TOKEN: Telegram Bot API token from @BotFather
* - TELEGRAM_NOTIFICATION: Enable/disable Telegram notifications (default: true) * - TELEGRAM_NOTIFICATION: Enable/disable Telegram notifications (default: true)
* - WEBAPP_PORT: Port for Mini App HTTP server (default: 3006) * - HAPI_LISTEN_HOST: Host/IP to bind the HTTP server (default: 127.0.0.1)
* - WEBAPP_URL: Public URL for Telegram Mini App * - HAPI_LISTEN_PORT: Port for HTTP server (default: 3006)
* - HAPI_PUBLIC_URL: Public URL for external access (e.g., Telegram Mini App)
* - CORS_ORIGINS: Comma-separated CORS origins * - CORS_ORIGINS: Comma-separated CORS origins
* - HAPI_RELAY_API: Relay API domain for tunwg (default: relay.hapi.run) * - HAPI_RELAY_API: Relay API domain for tunwg (default: relay.hapi.run)
* - HAPI_RELAY_AUTH: Relay auth key for tunwg (default: hapi) * - HAPI_RELAY_AUTH: Relay auth key for tunwg (default: hapi)
@@ -32,9 +33,9 @@ export type ConfigSource = 'env' | 'file' | 'default'
export interface ConfigSources { export interface ConfigSources {
telegramBotToken: ConfigSource telegramBotToken: ConfigSource
telegramNotification: ConfigSource telegramNotification: ConfigSource
webappHost: ConfigSource listenHost: ConfigSource
webappPort: ConfigSource listenPort: ConfigSource
webappUrl: ConfigSource publicUrl: ConfigSource
corsOrigins: ConfigSource corsOrigins: ConfigSource
cliApiToken: 'env' | 'file' | 'generated' cliApiToken: 'env' | 'file' | 'generated'
} }
@@ -67,14 +68,14 @@ class Configuration {
/** SQLite DB path */ /** SQLite DB path */
public readonly dbPath: string public readonly dbPath: string
/** Port for the Mini App HTTP server */ /** Port for the HTTP server */
public readonly webappPort: number public readonly listenPort: number
/** Host/IP to bind the Mini App HTTP server to */ /** Host/IP to bind the HTTP server to */
public readonly webappHost: string public readonly listenHost: string
/** Public HTTPS URL for the Telegram Mini App (used in WebApp buttons) */ /** Public URL for external access (e.g., Telegram Mini App) */
public readonly miniAppUrl: string public readonly publicUrl: string
/** Allowed CORS origins for Mini App + Socket.IO (comma-separated env override) */ /** Allowed CORS origins for Mini App + Socket.IO (comma-separated env override) */
public readonly corsOrigins: string[] public readonly corsOrigins: string[]
@@ -97,9 +98,9 @@ class Configuration {
this.telegramBotToken = serverSettings.telegramBotToken this.telegramBotToken = serverSettings.telegramBotToken
this.telegramEnabled = Boolean(this.telegramBotToken) this.telegramEnabled = Boolean(this.telegramBotToken)
this.telegramNotification = serverSettings.telegramNotification this.telegramNotification = serverSettings.telegramNotification
this.webappHost = serverSettings.webappHost this.listenHost = serverSettings.listenHost
this.webappPort = serverSettings.webappPort this.listenPort = serverSettings.listenPort
this.miniAppUrl = serverSettings.webappUrl this.publicUrl = serverSettings.publicUrl
this.corsOrigins = serverSettings.corsOrigins this.corsOrigins = serverSettings.corsOrigins
// CLI API token - will be set by _setCliApiToken() before create() returns // CLI API token - will be set by _setCliApiToken() before create() returns
+8 -8
View File
@@ -137,9 +137,9 @@ async function main() {
} }
// Display other configuration sources // Display other configuration sources
console.log(`[Server] WEBAPP_HOST: ${config.webappHost} (${formatSource(config.sources.webappHost)})`) console.log(`[Server] HAPI_LISTEN_HOST: ${config.listenHost} (${formatSource(config.sources.listenHost)})`)
console.log(`[Server] WEBAPP_PORT: ${config.webappPort} (${formatSource(config.sources.webappPort)})`) console.log(`[Server] HAPI_LISTEN_PORT: ${config.listenPort} (${formatSource(config.sources.listenPort)})`)
console.log(`[Server] WEBAPP_URL: ${config.miniAppUrl} (${formatSource(config.sources.webappUrl)})`) console.log(`[Server] HAPI_PUBLIC_URL: ${config.publicUrl} (${formatSource(config.sources.publicUrl)})`)
if (!config.telegramEnabled) { if (!config.telegramEnabled) {
console.log('[Server] Telegram: disabled (no TELEGRAM_BOT_TOKEN)') console.log('[Server] Telegram: disabled (no TELEGRAM_BOT_TOKEN)')
@@ -180,7 +180,7 @@ async function main() {
syncEngine = new SyncEngine(store, socketServer.io, socketServer.rpcRegistry, sseManager) syncEngine = new SyncEngine(store, socketServer.io, socketServer.rpcRegistry, sseManager)
const notificationChannels: NotificationChannel[] = [ const notificationChannels: NotificationChannel[] = [
new PushNotificationChannel(pushService, sseManager, visibilityTracker, config.miniAppUrl) new PushNotificationChannel(pushService, sseManager, visibilityTracker, config.publicUrl)
] ]
// Initialize Telegram bot (optional) // Initialize Telegram bot (optional)
@@ -188,7 +188,7 @@ async function main() {
happyBot = new HappyBot({ happyBot = new HappyBot({
syncEngine, syncEngine,
botToken: config.telegramBotToken, botToken: config.telegramBotToken,
miniAppUrl: config.miniAppUrl, publicUrl: config.publicUrl,
store store
}) })
// Only add to notification channels if notifications are enabled // Only add to notification channels if notifications are enabled
@@ -219,14 +219,14 @@ async function main() {
} }
console.log('') console.log('')
console.log('[Web] Server listening on :' + config.webappPort) console.log('[Web] Server listening on :' + config.listenPort)
console.log('[Web] Local: http://localhost:' + config.webappPort) console.log('[Web] Local: http://localhost:' + config.listenPort)
// Initialize tunnel AFTER web server is ready // Initialize tunnel AFTER web server is ready
let tunnelUrl: string | null = null let tunnelUrl: string | null = null
if (relayFlag.enabled) { if (relayFlag.enabled) {
tunnelManager = new TunnelManager({ tunnelManager = new TunnelManager({
localPort: config.webappPort, localPort: config.listenPort,
enabled: true, enabled: true,
apiDomain: relayApiDomain, apiDomain: relayApiDomain,
authKey: process.env.HAPI_RELAY_AUTH || null, authKey: process.env.HAPI_RELAY_AUTH || null,
+7 -7
View File
@@ -20,7 +20,7 @@ export interface BotContext extends Context {
export interface HappyBotConfig { export interface HappyBotConfig {
syncEngine: SyncEngine syncEngine: SyncEngine
botToken: string botToken: string
miniAppUrl: string publicUrl: string
store: Store store: Store
} }
@@ -31,12 +31,12 @@ export class HappyBot implements NotificationChannel {
private bot: Bot<BotContext> private bot: Bot<BotContext>
private syncEngine: SyncEngine | null = null private syncEngine: SyncEngine | null = null
private isRunning = false private isRunning = false
private readonly miniAppUrl: string private readonly publicUrl: string
private readonly store: Store private readonly store: Store
constructor(config: HappyBotConfig) { constructor(config: HappyBotConfig) {
this.syncEngine = config.syncEngine this.syncEngine = config.syncEngine
this.miniAppUrl = config.miniAppUrl this.publicUrl = config.publicUrl
this.store = config.store this.store = config.store
this.bot = new Bot<BotContext>(config.botToken) this.bot = new Bot<BotContext>(config.botToken)
@@ -108,13 +108,13 @@ export class HappyBot implements NotificationChannel {
private setupCommands(): void { private setupCommands(): void {
// /app - Open Telegram Mini App (primary entry point) // /app - Open Telegram Mini App (primary entry point)
this.bot.command('app', async (ctx) => { this.bot.command('app', async (ctx) => {
const keyboard = new InlineKeyboard().webApp('Open App', this.miniAppUrl) const keyboard = new InlineKeyboard().webApp('Open App', this.publicUrl)
await ctx.reply('Open HAPI Mini App:', { reply_markup: keyboard }) await ctx.reply('Open HAPI Mini App:', { reply_markup: keyboard })
}) })
// /start - Simple welcome with Mini App link // /start - Simple welcome with Mini App link
this.bot.command('start', async (ctx) => { this.bot.command('start', async (ctx) => {
const keyboard = new InlineKeyboard().webApp('Open App', this.miniAppUrl) const keyboard = new InlineKeyboard().webApp('Open App', this.publicUrl)
await ctx.reply( await ctx.reply(
'Welcome to HAPI Bot!\n\n' + 'Welcome to HAPI Bot!\n\n' +
'Use the Mini App for full session management.', 'Use the Mini App for full session management.',
@@ -190,7 +190,7 @@ export class HappyBot implements NotificationChannel {
} }
const agentName = getAgentName(session) const agentName = getAgentName(session)
const url = buildMiniAppDeepLink(this.miniAppUrl, `session_${session.id}`) const url = buildMiniAppDeepLink(this.publicUrl, `session_${session.id}`)
const keyboard = new InlineKeyboard() const keyboard = new InlineKeyboard()
.webApp('Open Session', url) .webApp('Open Session', url)
@@ -221,7 +221,7 @@ export class HappyBot implements NotificationChannel {
} }
const text = formatSessionNotification(session) const text = formatSessionNotification(session)
const keyboard = createNotificationKeyboard(session, this.miniAppUrl) const keyboard = createNotificationKeyboard(session, this.publicUrl)
const chatIds = this.getBoundChatIds(session.namespace) const chatIds = this.getBoundChatIds(session.namespace)
if (chatIds.length === 0) { if (chatIds.length === 0) {
+3 -3
View File
@@ -38,7 +38,7 @@ export function formatSessionNotification(session: Session): string {
/** /**
* Create notification keyboard for quick actions * Create notification keyboard for quick actions
*/ */
export function createNotificationKeyboard(session: Session, miniAppUrl: string): InlineKeyboard { export function createNotificationKeyboard(session: Session, publicUrl: string): InlineKeyboard {
const keyboard = new InlineKeyboard() const keyboard = new InlineKeyboard()
const requests = session.agentState?.requests ?? null const requests = session.agentState?.requests ?? null
const hasRequests = Boolean(requests && Object.keys(requests).length > 0) const hasRequests = Boolean(requests && Object.keys(requests).length > 0)
@@ -55,14 +55,14 @@ export function createNotificationKeyboard(session: Session, miniAppUrl: string)
keyboard.webApp( keyboard.webApp(
'Details', 'Details',
buildMiniAppDeepLink(miniAppUrl, `session_${session.id}`) buildMiniAppDeepLink(publicUrl, `session_${session.id}`)
) )
return keyboard return keyboard
} }
keyboard.webApp( keyboard.webApp(
'Open Session', 'Open Session',
buildMiniAppDeepLink(miniAppUrl, `session_${session.id}`) buildMiniAppDeepLink(publicUrl, `session_${session.id}`)
) )
return keyboard return keyboard
} }
+4 -4
View File
@@ -228,8 +228,8 @@ export async function startWebServer(options: {
const socketHandler = options.socketEngine.handler() const socketHandler = options.socketEngine.handler()
const server = Bun.serve({ const server = Bun.serve({
hostname: configuration.webappHost, hostname: configuration.listenHost,
port: configuration.webappPort, port: configuration.listenPort,
idleTimeout: Math.max(30, socketHandler.idleTimeout), idleTimeout: Math.max(30, socketHandler.idleTimeout),
maxRequestBodySize: socketHandler.maxRequestBodySize, maxRequestBodySize: socketHandler.maxRequestBodySize,
websocket: socketHandler.websocket, websocket: socketHandler.websocket,
@@ -242,8 +242,8 @@ export async function startWebServer(options: {
} }
}) })
console.log(`[Web] Mini App server listening on ${configuration.webappHost}:${configuration.webappPort}`) console.log(`[Web] server listening on ${configuration.listenHost}:${configuration.listenPort}`)
console.log(`[Web] Mini App public URL: ${configuration.miniAppUrl}`) console.log(`[Web] public URL: ${configuration.publicUrl}`)
return server return server
} }