Merge remote-tracking branch 'origin/fix/server-api-config' into HEAD
# Conflicts: # docs/configuration.md # src/config/io.ts # src/main.ts
This commit is contained in:
@@ -23,6 +23,10 @@ For global installs (`npm install -g`), either:
|
|||||||
server:
|
server:
|
||||||
mode: api # 'api' or 'docker' (legacy: 'cloud'/'selfhosted')
|
mode: api # 'api' or 'docker' (legacy: 'cloud'/'selfhosted')
|
||||||
apiKey: letta_... # Required for api mode
|
apiKey: letta_... # Required for api mode
|
||||||
|
api:
|
||||||
|
port: 8080 # Default: 8080 (or PORT env var)
|
||||||
|
# host: 0.0.0.0 # Uncomment for Docker/Railway
|
||||||
|
# corsOrigin: https://my.app # Uncomment for cross-origin access
|
||||||
|
|
||||||
# Agent settings (single agent mode)
|
# Agent settings (single agent mode)
|
||||||
# For multiple agents, use `agents:` array instead -- see Multi-Agent section
|
# For multiple agents, use `agents:` array instead -- see Multi-Agent section
|
||||||
@@ -87,11 +91,6 @@ attachments:
|
|||||||
maxMB: 20
|
maxMB: 20
|
||||||
maxAgeDays: 14
|
maxAgeDays: 14
|
||||||
|
|
||||||
# API server (health checks, CLI messaging)
|
|
||||||
api:
|
|
||||||
port: 8080 # Default: 8080 (or PORT env var)
|
|
||||||
# host: 0.0.0.0 # Uncomment for Docker/Railway
|
|
||||||
# corsOrigin: https://my.app # Uncomment for cross-origin access
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Server Configuration
|
## Server Configuration
|
||||||
@@ -226,7 +225,7 @@ agents:
|
|||||||
cron: true
|
cron: true
|
||||||
```
|
```
|
||||||
|
|
||||||
The `server:`, `transcription:`, `attachments:`, and `api:` sections remain at the top level (shared across all agents).
|
The `server:` (including `server.api:`), `transcription:`, and `attachments:` sections remain at the top level (shared across all agents).
|
||||||
|
|
||||||
### Known limitations
|
### Known limitations
|
||||||
|
|
||||||
@@ -449,9 +448,9 @@ The top-level `polling` section takes priority if both are present.
|
|||||||
|--------------|--------------------------|
|
|--------------|--------------------------|
|
||||||
| `GMAIL_ACCOUNT` | `polling.gmail.account` (comma-separated list allowed) |
|
| `GMAIL_ACCOUNT` | `polling.gmail.account` (comma-separated list allowed) |
|
||||||
| `POLLING_INTERVAL_MS` | `polling.intervalMs` |
|
| `POLLING_INTERVAL_MS` | `polling.intervalMs` |
|
||||||
| `PORT` | `api.port` |
|
| `PORT` | `server.api.port` |
|
||||||
| `API_HOST` | `api.host` |
|
| `API_HOST` | `server.api.host` |
|
||||||
| `API_CORS_ORIGIN` | `api.corsOrigin` |
|
| `API_CORS_ORIGIN` | `server.api.corsOrigin` |
|
||||||
|
|
||||||
## Transcription Configuration
|
## Transcription Configuration
|
||||||
|
|
||||||
@@ -478,18 +477,25 @@ Attachments are stored in `/tmp/lettabot/attachments/`.
|
|||||||
|
|
||||||
The built-in API server provides health checks, CLI messaging, and a chat endpoint for programmatic agent access.
|
The built-in API server provides health checks, CLI messaging, and a chat endpoint for programmatic agent access.
|
||||||
|
|
||||||
|
Configure it under `server.api:` in your `lettabot.yaml`:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
api:
|
server:
|
||||||
port: 9090 # Default: 8080
|
mode: docker
|
||||||
host: 0.0.0.0 # Default: 127.0.0.1 (localhost only)
|
baseUrl: http://localhost:8283
|
||||||
corsOrigin: "*" # Default: same-origin only
|
api:
|
||||||
|
port: 9090 # Default: 8080
|
||||||
|
host: 0.0.0.0 # Default: 127.0.0.1 (localhost only)
|
||||||
|
corsOrigin: "*" # Default: same-origin only
|
||||||
```
|
```
|
||||||
|
|
||||||
| Option | Type | Default | Description |
|
| Option | Type | Default | Description |
|
||||||
|--------|------|---------|-------------|
|
|--------|------|---------|-------------|
|
||||||
| `api.port` | number | `8080` | Port for the API/health server |
|
| `server.api.port` | number | `8080` | Port for the API/health server |
|
||||||
| `api.host` | string | `127.0.0.1` | Bind address. Use `0.0.0.0` for Docker/Railway |
|
| `server.api.host` | string | `127.0.0.1` | Bind address. Use `0.0.0.0` for Docker/Railway |
|
||||||
| `api.corsOrigin` | string | _(none)_ | CORS origin header for cross-origin access |
|
| `server.api.corsOrigin` | string | _(none)_ | CORS origin header for cross-origin access |
|
||||||
|
|
||||||
|
> **Note:** Top-level `api:` is still accepted for backward compatibility but deprecated. Move it under `server:` to avoid warnings.
|
||||||
|
|
||||||
### Chat Endpoint
|
### Chat Endpoint
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||||
import { mkdtempSync, existsSync, readFileSync, rmSync } from 'node:fs';
|
import { mkdtempSync, existsSync, readFileSync, writeFileSync, rmSync } from 'node:fs';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import { tmpdir } from 'node:os';
|
import { tmpdir } from 'node:os';
|
||||||
import YAML from 'yaml';
|
import YAML from 'yaml';
|
||||||
import { saveConfig, loadConfig } from './io.js';
|
import { saveConfig, loadConfig, configToEnv, didLoadFail } from './io.js';
|
||||||
import { normalizeAgents } from './types.js';
|
import { normalizeAgents, DEFAULT_CONFIG } from './types.js';
|
||||||
import type { LettaBotConfig } from './types.js';
|
import type { LettaBotConfig } from './types.js';
|
||||||
|
|
||||||
describe('saveConfig with agents[] format', () => {
|
describe('saveConfig with agents[] format', () => {
|
||||||
@@ -149,3 +149,229 @@ describe('saveConfig with agents[] format', () => {
|
|||||||
expect(parsed.agents[0].providers).toBeUndefined();
|
expect(parsed.agents[0].providers).toBeUndefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('server.api config (canonical location)', () => {
|
||||||
|
it('configToEnv should read port from server.api', () => {
|
||||||
|
const config: LettaBotConfig = {
|
||||||
|
...DEFAULT_CONFIG,
|
||||||
|
server: {
|
||||||
|
mode: 'selfhosted',
|
||||||
|
baseUrl: 'http://localhost:6701',
|
||||||
|
api: { port: 6702, host: '0.0.0.0', corsOrigin: '*' },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const env = configToEnv(config);
|
||||||
|
|
||||||
|
expect(env.PORT).toBe('6702');
|
||||||
|
expect(env.API_HOST).toBe('0.0.0.0');
|
||||||
|
expect(env.API_CORS_ORIGIN).toBe('*');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('configToEnv should fall back to top-level api (deprecated)', () => {
|
||||||
|
const config: LettaBotConfig = {
|
||||||
|
...DEFAULT_CONFIG,
|
||||||
|
server: { mode: 'selfhosted', baseUrl: 'http://localhost:6701' },
|
||||||
|
api: { port: 8081 },
|
||||||
|
};
|
||||||
|
|
||||||
|
const env = configToEnv(config);
|
||||||
|
|
||||||
|
expect(env.PORT).toBe('8081');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('server.api should take precedence over top-level api', () => {
|
||||||
|
const config: LettaBotConfig = {
|
||||||
|
...DEFAULT_CONFIG,
|
||||||
|
server: {
|
||||||
|
mode: 'selfhosted',
|
||||||
|
baseUrl: 'http://localhost:6701',
|
||||||
|
api: { port: 9090 },
|
||||||
|
},
|
||||||
|
api: { port: 8081 },
|
||||||
|
};
|
||||||
|
|
||||||
|
const env = configToEnv(config);
|
||||||
|
|
||||||
|
expect(env.PORT).toBe('9090');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not set PORT when no api config is present', () => {
|
||||||
|
const config: LettaBotConfig = {
|
||||||
|
...DEFAULT_CONFIG,
|
||||||
|
server: { mode: 'selfhosted', baseUrl: 'http://localhost:6701' },
|
||||||
|
};
|
||||||
|
|
||||||
|
const env = configToEnv(config);
|
||||||
|
|
||||||
|
expect(env.PORT).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('server.api should survive save/load roundtrip in YAML', () => {
|
||||||
|
const tmpDir = mkdtempSync(join(tmpdir(), 'lettabot-api-test-'));
|
||||||
|
const configPath = join(tmpDir, 'lettabot.yaml');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const config = {
|
||||||
|
server: {
|
||||||
|
mode: 'selfhosted' as const,
|
||||||
|
baseUrl: 'http://localhost:6701',
|
||||||
|
api: { port: 6702, host: '0.0.0.0' },
|
||||||
|
},
|
||||||
|
agents: [{
|
||||||
|
name: 'TestBot',
|
||||||
|
channels: {},
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
|
||||||
|
saveConfig(config, configPath);
|
||||||
|
|
||||||
|
const raw = readFileSync(configPath, 'utf-8');
|
||||||
|
const parsed = YAML.parse(raw);
|
||||||
|
|
||||||
|
// server.api should be in the YAML under server
|
||||||
|
expect(parsed.server.api).toBeDefined();
|
||||||
|
expect(parsed.server.api.port).toBe(6702);
|
||||||
|
expect(parsed.server.api.host).toBe('0.0.0.0');
|
||||||
|
|
||||||
|
// Should NOT have top-level api
|
||||||
|
expect(parsed.api).toBeUndefined();
|
||||||
|
} finally {
|
||||||
|
rmSync(tmpDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('didLoadFail', () => {
|
||||||
|
it('should return false initially', () => {
|
||||||
|
// loadConfig hasn't been called with a bad file, so it should be false
|
||||||
|
// (or whatever state it was left in from previous test)
|
||||||
|
// Call loadConfig with a valid env to reset
|
||||||
|
const originalEnv = process.env.LETTABOT_CONFIG;
|
||||||
|
const tmpDir = mkdtempSync(join(tmpdir(), 'lettabot-fail-test-'));
|
||||||
|
const configPath = join(tmpDir, 'lettabot.yaml');
|
||||||
|
|
||||||
|
try {
|
||||||
|
writeFileSync(configPath, 'server:\n mode: cloud\n', 'utf-8');
|
||||||
|
process.env.LETTABOT_CONFIG = configPath;
|
||||||
|
loadConfig();
|
||||||
|
expect(didLoadFail()).toBe(false);
|
||||||
|
} finally {
|
||||||
|
process.env.LETTABOT_CONFIG = originalEnv;
|
||||||
|
rmSync(tmpDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true after a parse error', () => {
|
||||||
|
const originalEnv = process.env.LETTABOT_CONFIG;
|
||||||
|
const tmpDir = mkdtempSync(join(tmpdir(), 'lettabot-fail-test-'));
|
||||||
|
const configPath = join(tmpDir, 'lettabot.yaml');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Write invalid YAML
|
||||||
|
writeFileSync(configPath, 'server:\n api: port: 6702\n', 'utf-8');
|
||||||
|
process.env.LETTABOT_CONFIG = configPath;
|
||||||
|
|
||||||
|
// Suppress console output during test
|
||||||
|
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
|
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||||
|
|
||||||
|
const config = loadConfig();
|
||||||
|
|
||||||
|
expect(didLoadFail()).toBe(true);
|
||||||
|
// Should return default config on failure
|
||||||
|
expect(config.server.mode).toBe(DEFAULT_CONFIG.server.mode);
|
||||||
|
|
||||||
|
errorSpy.mockRestore();
|
||||||
|
warnSpy.mockRestore();
|
||||||
|
} finally {
|
||||||
|
process.env.LETTABOT_CONFIG = originalEnv;
|
||||||
|
rmSync(tmpDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reset to false on successful load after a failure', () => {
|
||||||
|
const originalEnv = process.env.LETTABOT_CONFIG;
|
||||||
|
const tmpDir = mkdtempSync(join(tmpdir(), 'lettabot-fail-test-'));
|
||||||
|
const badPath = join(tmpDir, 'bad.yaml');
|
||||||
|
const goodPath = join(tmpDir, 'good.yaml');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// First: load bad file
|
||||||
|
writeFileSync(badPath, 'server:\n api: port: 6702\n', 'utf-8');
|
||||||
|
process.env.LETTABOT_CONFIG = badPath;
|
||||||
|
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
|
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||||
|
loadConfig();
|
||||||
|
expect(didLoadFail()).toBe(true);
|
||||||
|
errorSpy.mockRestore();
|
||||||
|
warnSpy.mockRestore();
|
||||||
|
|
||||||
|
// Then: load good file
|
||||||
|
writeFileSync(goodPath, 'server:\n mode: selfhosted\n baseUrl: http://localhost:6701\n', 'utf-8');
|
||||||
|
process.env.LETTABOT_CONFIG = goodPath;
|
||||||
|
loadConfig();
|
||||||
|
expect(didLoadFail()).toBe(false);
|
||||||
|
} finally {
|
||||||
|
process.env.LETTABOT_CONFIG = originalEnv;
|
||||||
|
rmSync(tmpDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('loadConfig deprecation warning for top-level api', () => {
|
||||||
|
it('should warn when top-level api is present without server.api', () => {
|
||||||
|
const originalEnv = process.env.LETTABOT_CONFIG;
|
||||||
|
const tmpDir = mkdtempSync(join(tmpdir(), 'lettabot-deprecation-test-'));
|
||||||
|
const configPath = join(tmpDir, 'lettabot.yaml');
|
||||||
|
|
||||||
|
try {
|
||||||
|
writeFileSync(configPath, 'server:\n mode: cloud\napi:\n port: 9090\n', 'utf-8');
|
||||||
|
process.env.LETTABOT_CONFIG = configPath;
|
||||||
|
|
||||||
|
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||||
|
|
||||||
|
const config = loadConfig();
|
||||||
|
|
||||||
|
expect(warnSpy).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining('Top-level `api:` is deprecated')
|
||||||
|
);
|
||||||
|
// The top-level api should still be loaded
|
||||||
|
expect(config.api?.port).toBe(9090);
|
||||||
|
|
||||||
|
warnSpy.mockRestore();
|
||||||
|
} finally {
|
||||||
|
process.env.LETTABOT_CONFIG = originalEnv;
|
||||||
|
rmSync(tmpDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not warn when server.api is used (canonical location)', () => {
|
||||||
|
const originalEnv = process.env.LETTABOT_CONFIG;
|
||||||
|
const tmpDir = mkdtempSync(join(tmpdir(), 'lettabot-deprecation-test-'));
|
||||||
|
const configPath = join(tmpDir, 'lettabot.yaml');
|
||||||
|
|
||||||
|
try {
|
||||||
|
writeFileSync(configPath, 'server:\n mode: selfhosted\n baseUrl: http://localhost:6701\n api:\n port: 6702\n', 'utf-8');
|
||||||
|
process.env.LETTABOT_CONFIG = configPath;
|
||||||
|
|
||||||
|
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||||
|
|
||||||
|
const config = loadConfig();
|
||||||
|
|
||||||
|
// Should NOT have deprecated warning
|
||||||
|
expect(warnSpy).not.toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining('Top-level `api:` is deprecated')
|
||||||
|
);
|
||||||
|
// server.api should be loaded
|
||||||
|
expect(config.server.api?.port).toBe(6702);
|
||||||
|
// top-level api should be undefined
|
||||||
|
expect(config.api).toBeUndefined();
|
||||||
|
|
||||||
|
warnSpy.mockRestore();
|
||||||
|
} finally {
|
||||||
|
process.env.LETTABOT_CONFIG = originalEnv;
|
||||||
|
rmSync(tmpDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -46,10 +46,18 @@ export function resolveConfigPath(): string {
|
|||||||
return DEFAULT_CONFIG_PATH;
|
return DEFAULT_CONFIG_PATH;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the last loadConfig() call failed to parse the config file.
|
||||||
|
* Used to avoid misleading "Loaded from" messages when the file exists but has syntax errors.
|
||||||
|
*/
|
||||||
|
let _lastLoadFailed = false;
|
||||||
|
export function didLoadFail(): boolean { return _lastLoadFailed; }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load config from YAML file
|
* Load config from YAML file
|
||||||
*/
|
*/
|
||||||
export function loadConfig(): LettaBotConfig {
|
export function loadConfig(): LettaBotConfig {
|
||||||
|
_lastLoadFailed = false;
|
||||||
const configPath = resolveConfigPath();
|
const configPath = resolveConfigPath();
|
||||||
|
|
||||||
if (!existsSync(configPath)) {
|
if (!existsSync(configPath)) {
|
||||||
@@ -74,15 +82,24 @@ export function loadConfig(): LettaBotConfig {
|
|||||||
channels: { ...DEFAULT_CONFIG.channels, ...parsed.channels },
|
channels: { ...DEFAULT_CONFIG.channels, ...parsed.channels },
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
const config = {
|
||||||
...merged,
|
...merged,
|
||||||
server: {
|
server: {
|
||||||
...merged.server,
|
...merged.server,
|
||||||
mode: canonicalizeServerMode(merged.server.mode),
|
mode: canonicalizeServerMode(merged.server.mode),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Deprecation warning: top-level api should be moved under server
|
||||||
|
if (config.api && !config.server.api) {
|
||||||
|
console.warn('[Config] WARNING: Top-level `api:` is deprecated. Move it under `server:`.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return config;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`[Config] Failed to load ${configPath}:`, err);
|
_lastLoadFailed = true;
|
||||||
|
console.error(`[Config] Failed to parse ${configPath}:`, err);
|
||||||
|
console.warn('[Config] Using default configuration. Check your YAML syntax.');
|
||||||
return { ...DEFAULT_CONFIG };
|
return { ...DEFAULT_CONFIG };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -292,15 +309,16 @@ export function configToEnv(config: LettaBotConfig): Record<string, string> {
|
|||||||
env.ATTACHMENTS_MAX_AGE_DAYS = String(config.attachments.maxAgeDays);
|
env.ATTACHMENTS_MAX_AGE_DAYS = String(config.attachments.maxAgeDays);
|
||||||
}
|
}
|
||||||
|
|
||||||
// API server
|
// API server (server.api is canonical, top-level api is deprecated fallback)
|
||||||
if (config.api?.port !== undefined) {
|
const apiConfig = config.server.api ?? config.api;
|
||||||
env.PORT = String(config.api.port);
|
if (apiConfig?.port !== undefined) {
|
||||||
|
env.PORT = String(apiConfig.port);
|
||||||
}
|
}
|
||||||
if (config.api?.host) {
|
if (apiConfig?.host) {
|
||||||
env.API_HOST = config.api.host;
|
env.API_HOST = apiConfig.host;
|
||||||
}
|
}
|
||||||
if (config.api?.corsOrigin) {
|
if (apiConfig?.corsOrigin) {
|
||||||
env.API_CORS_ORIGIN = config.api.corsOrigin;
|
env.API_CORS_ORIGIN = apiConfig.corsOrigin;
|
||||||
}
|
}
|
||||||
|
|
||||||
return env;
|
return env;
|
||||||
|
|||||||
@@ -76,6 +76,12 @@ export interface LettaBotConfig {
|
|||||||
baseUrl?: string;
|
baseUrl?: string;
|
||||||
// Only for api mode
|
// Only for api mode
|
||||||
apiKey?: string;
|
apiKey?: string;
|
||||||
|
// API server config (port, host, CORS) — canonical location
|
||||||
|
api?: {
|
||||||
|
port?: number; // Default: 8080 (or PORT env var)
|
||||||
|
host?: string; // Default: 127.0.0.1 (secure). Use '0.0.0.0' for Docker/Railway
|
||||||
|
corsOrigin?: string; // CORS origin. Default: same-origin only
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// Multi-agent configuration
|
// Multi-agent configuration
|
||||||
@@ -137,6 +143,7 @@ export interface LettaBotConfig {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// API server (health checks, CLI messaging)
|
// API server (health checks, CLI messaging)
|
||||||
|
/** @deprecated Use server.api instead */
|
||||||
api?: {
|
api?: {
|
||||||
port?: number; // Default: 8080 (or PORT env var)
|
port?: number; // Default: 8080 (or PORT env var)
|
||||||
host?: string; // Default: 127.0.0.1 (secure). Use '0.0.0.0' for Docker/Railway
|
host?: string; // Default: 127.0.0.1 (secure). Use '0.0.0.0' for Docker/Railway
|
||||||
|
|||||||
@@ -19,14 +19,19 @@ import {
|
|||||||
applyConfigToEnv,
|
applyConfigToEnv,
|
||||||
syncProviders,
|
syncProviders,
|
||||||
resolveConfigPath,
|
resolveConfigPath,
|
||||||
|
didLoadFail,
|
||||||
isDockerServerMode,
|
isDockerServerMode,
|
||||||
serverModeLabel,
|
serverModeLabel,
|
||||||
} from './config/index.js';
|
} from './config/index.js';
|
||||||
import { isLettaApiUrl } from './utils/server.js';
|
import { isLettaApiUrl } from './utils/server.js';
|
||||||
import { getDataDir, getWorkingDir, hasRailwayVolume } from './utils/paths.js';
|
import { getDataDir, getWorkingDir, hasRailwayVolume } from './utils/paths.js';
|
||||||
const yamlConfig = loadConfig();
|
const yamlConfig = loadConfig();
|
||||||
const configSource = existsSync(resolveConfigPath()) ? resolveConfigPath() : 'defaults + environment variables';
|
if (didLoadFail()) {
|
||||||
console.log(`[Config] Loaded from ${configSource}`);
|
console.warn(`[Config] Fix the errors above in ${resolveConfigPath()} and restart.`);
|
||||||
|
} else {
|
||||||
|
const configSource = existsSync(resolveConfigPath()) ? resolveConfigPath() : 'defaults + environment variables';
|
||||||
|
console.log(`[Config] Loaded from ${configSource}`);
|
||||||
|
}
|
||||||
if (yamlConfig.agents?.length) {
|
if (yamlConfig.agents?.length) {
|
||||||
console.log(`[Config] Mode: ${serverModeLabel(yamlConfig.server.mode)}, Agents: ${yamlConfig.agents.map(a => a.name).join(', ')}`);
|
console.log(`[Config] Mode: ${serverModeLabel(yamlConfig.server.mode)}, Agents: ${yamlConfig.agents.map(a => a.name).join(', ')}`);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -113,14 +113,18 @@ function readConfigFromEnv(existingConfig: any): any {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveConfigFromEnv(config: any, configPath: string): Promise<void> {
|
async function saveConfigFromEnv(config: any, configPath: string, existingConfig?: LettaBotConfig): Promise<void> {
|
||||||
const { saveConfig } = await import('./config/index.js');
|
const { saveConfig } = await import('./config/index.js');
|
||||||
|
|
||||||
|
// Resolve API server config from existing config (server.api is canonical, top-level api is fallback)
|
||||||
|
const existingApiConfig = existingConfig?.server?.api ?? existingConfig?.api;
|
||||||
|
|
||||||
const lettabotConfig: Partial<LettaBotConfig> & Pick<LettaBotConfig, 'server'> = {
|
const lettabotConfig: Partial<LettaBotConfig> & Pick<LettaBotConfig, 'server'> = {
|
||||||
server: {
|
server: {
|
||||||
mode: isLettaApiUrl(config.baseUrl) ? 'api' : 'docker',
|
mode: isLettaApiUrl(config.baseUrl) ? 'api' : 'docker',
|
||||||
baseUrl: config.baseUrl,
|
baseUrl: config.baseUrl,
|
||||||
apiKey: config.apiKey,
|
apiKey: config.apiKey,
|
||||||
|
...(existingApiConfig ? { api: existingApiConfig } : {}),
|
||||||
},
|
},
|
||||||
agents: [{
|
agents: [{
|
||||||
name: config.agentName,
|
name: config.agentName,
|
||||||
@@ -196,6 +200,8 @@ async function saveConfigFromEnv(config: any, configPath: string): Promise<void>
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}],
|
}],
|
||||||
|
// Preserve unmanaged top-level fields from existing config
|
||||||
|
...(existingConfig?.attachments ? { attachments: existingConfig.attachments } : {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
saveConfig(lettabotConfig);
|
saveConfig(lettabotConfig);
|
||||||
@@ -1372,8 +1378,8 @@ export async function onboard(options?: { nonInteractive?: boolean }): Promise<v
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save config and exit
|
// Save config and exit (pass existingConfig to preserve unmanaged fields like api/attachments)
|
||||||
await saveConfigFromEnv(config, configPath);
|
await saveConfigFromEnv(config, configPath, existingConfig);
|
||||||
console.log(`✅ Configuration saved to ${configPath}\n`);
|
console.log(`✅ Configuration saved to ${configPath}\n`);
|
||||||
console.log('Run "lettabot server" to start the bot.');
|
console.log('Run "lettabot server" to start the bot.');
|
||||||
return;
|
return;
|
||||||
@@ -1762,11 +1768,16 @@ export async function onboard(options?: { nonInteractive?: boolean }): Promise<v
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Convert to YAML config (multi-agent format)
|
// Convert to YAML config (multi-agent format)
|
||||||
|
// Resolve API server config from existing config (server.api is canonical, top-level api is fallback)
|
||||||
|
const existingApiConfig = existingConfig.server?.api ?? existingConfig.api;
|
||||||
|
|
||||||
const yamlConfig: Partial<LettaBotConfig> & Pick<LettaBotConfig, 'server'> = {
|
const yamlConfig: Partial<LettaBotConfig> & Pick<LettaBotConfig, 'server'> = {
|
||||||
server: {
|
server: {
|
||||||
mode: isDockerAuthMethod(config.authMethod) ? 'docker' : 'api',
|
mode: isDockerAuthMethod(config.authMethod) ? 'docker' : 'api',
|
||||||
...(isDockerAuthMethod(config.authMethod) && config.baseUrl ? { baseUrl: config.baseUrl } : {}),
|
...(isDockerAuthMethod(config.authMethod) && config.baseUrl ? { baseUrl: config.baseUrl } : {}),
|
||||||
...(config.apiKey ? { apiKey: config.apiKey } : {}),
|
...(config.apiKey ? { apiKey: config.apiKey } : {}),
|
||||||
|
// Preserve API server config (port, host, CORS)
|
||||||
|
...(existingApiConfig ? { api: existingApiConfig } : {}),
|
||||||
},
|
},
|
||||||
agents: [agentConfig],
|
agents: [agentConfig],
|
||||||
...(config.transcription.enabled && config.transcription.apiKey ? {
|
...(config.transcription.enabled && config.transcription.apiKey ? {
|
||||||
@@ -1784,6 +1795,8 @@ export async function onboard(options?: { nonInteractive?: boolean }): Promise<v
|
|||||||
apiKey: p.apiKey,
|
apiKey: p.apiKey,
|
||||||
})),
|
})),
|
||||||
} : {}),
|
} : {}),
|
||||||
|
// Preserve unmanaged top-level fields from existing config
|
||||||
|
...(existingConfig.attachments ? { attachments: existingConfig.attachments } : {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Save YAML config (use project-local path)
|
// Save YAML config (use project-local path)
|
||||||
|
|||||||
Reference in New Issue
Block a user