Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ node_modules/
dist/
logs/
/data/*
!/data/default/
!/data/default/**
state.json
*.log

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,8 +189,8 @@ Persona and heartbeat prompts use a **default + user override** pattern:

| Default (git-tracked) | User override (gitignored) |
|------------------------|---------------------------|
| `data/default/persona.default.md` | `data/brain/persona.md` |
| `data/default/heartbeat.default.md` | `data/brain/heartbeat.md` |
| `default/persona.default.md` | `data/brain/persona.md` |
| `default/heartbeat.default.md` | `data/brain/heartbeat.md` |

On first run, defaults are auto-copied to the user override path. Edit the user files to customize without touching version control.

Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
35 changes: 35 additions & 0 deletions src/connectors/web/routes/trading-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
readAccountsConfig, writeAccountsConfig,
platformConfigSchema, accountConfigSchema,
} from '../../../core/config.js'
import { createPlatformFromConfig, createBrokerFromConfig } from '../../../domain/trading/brokers/factory.js'

/** Mask a secret string: show last 4 chars, prefix with "****" */
function mask(value: string | undefined): string | undefined {
Expand Down Expand Up @@ -153,5 +154,39 @@ export function createTradingConfigRoutes(ctx: EngineContext) {
}
})

// ==================== Test Connection ====================

app.post('/test-connection', async (c) => {
let broker: { init: () => Promise<void>; getAccount: () => Promise<unknown>; close: () => Promise<void> } | null = null
try {
const body = await c.req.json()
const platformConfig = platformConfigSchema.parse(body.platform)
const { apiKey, apiSecret, password } = body.credentials ?? {}

if (!apiKey || !apiSecret) {
return c.json({ success: false, error: 'API key and secret are required' }, 400)
}

const platform = createPlatformFromConfig(platformConfig)
broker = createBrokerFromConfig(platform, {
id: '__test__',
platformId: platformConfig.id,
apiKey,
apiSecret,
password,
guards: [],
})

await broker.init()
const account = await broker.getAccount()
return c.json({ success: true, account })
} catch (err) {
const msg = err instanceof Error ? err.message : String(err)
return c.json({ success: false, error: msg }, 400)
} finally {
try { await broker?.close() } catch { /* best effort */ }
}
})

return app
}
55 changes: 5 additions & 50 deletions src/core/config.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,63 +308,18 @@ describe('loadTradingConfig', () => {
expect(mockWriteFile).not.toHaveBeenCalled()
})

it('migrates from crypto.json + securities.json when platforms.json is missing', async () => {
// platforms.json → ENOENT
fileNotFound()
// accounts.json → ENOENT
fileNotFound()
// crypto.json (loaded inside migrateLegacyTradingConfig)
fileReturns({
provider: {
type: 'ccxt',
exchange: 'binance',
apiKey: 'k1',
apiSecret: 's1',
sandbox: false,
demoTrading: false,
},
guards: [],
})
// securities.json
fileReturns({
provider: { type: 'alpaca', paper: true, apiKey: 'alpk', secretKey: 'alps' },
guards: [],
})

const { platforms, accounts } = await loadTradingConfig()

expect(platforms.find(p => p.type === 'ccxt')).toBeDefined()
expect(platforms.find(p => p.type === 'alpaca')).toBeDefined()
expect(accounts.find(a => a.id === 'binance-main')).toBeDefined()
expect(accounts.find(a => a.id === 'alpaca-paper')).toBeDefined()

// Should have written platforms.json and accounts.json
const writtenPaths = mockWriteFile.mock.calls.map(c => c[0] as string)
expect(writtenPaths.some(p => p.endsWith('platforms.json'))).toBe(true)
expect(writtenPaths.some(p => p.endsWith('accounts.json'))).toBe(true)
})

it('migrates from legacy with none providers → empty arrays', async () => {
it('seeds empty arrays when config files are missing', async () => {
fileNotFound() // platforms.json
fileNotFound() // accounts.json
fileReturns({ provider: { type: 'none' }, guards: [] }) // crypto.json
fileReturns({ provider: { type: 'none' }, guards: [] }) // securities.json

const { platforms, accounts } = await loadTradingConfig()
expect(platforms).toHaveLength(0)
expect(accounts).toHaveLength(0)
})

it('falls back to defaults when legacy files are also missing', async () => {
fileNotFound() // platforms.json
fileNotFound() // accounts.json
fileNotFound() // crypto.json
fileNotFound() // securities.json

const { platforms, accounts } = await loadTradingConfig()
// Default crypto is ccxt/binance, default securities is alpaca/paper
expect(platforms.find(p => p.type === 'ccxt')).toBeDefined()
expect(platforms.find(p => p.type === 'alpaca')).toBeDefined()
// Should have written empty platforms.json and accounts.json
const writtenPaths = mockWriteFile.mock.calls.map(c => c[0] as string)
expect(writtenPaths.some(p => p.endsWith('platforms.json'))).toBe(true)
expect(writtenPaths.some(p => p.endsWith('accounts.json'))).toBe(true)
})
})

Expand Down
91 changes: 14 additions & 77 deletions src/core/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,7 @@ const cryptoSchema = z.object({
z.object({
type: z.literal('none'),
}),
]).default({
type: 'ccxt', exchange: 'binance', sandbox: false, demoTrading: true,
}),
]).default({ type: 'none' }),
guards: z.array(z.object({
type: z.string(),
options: z.record(z.string(), z.unknown()).default({}),
Expand All @@ -89,7 +87,7 @@ const securitiesSchema = z.object({
z.object({
type: z.literal('none'),
}),
]).default({ type: 'alpaca', paper: true }),
]).default({ type: 'none' }),
guards: z.array(z.object({
type: z.string(),
options: z.record(z.string(), z.unknown()).default({}),
Expand Down Expand Up @@ -162,7 +160,7 @@ const connectorsSchema = z.object({
const heartbeatSchema = z.object({
enabled: z.boolean().default(false),
every: z.string().default('30m'),
prompt: z.string().default('Read data/brain/heartbeat.md (or data/default/heartbeat.default.md if not found) and follow the instructions inside.'),
prompt: z.string().default('Read data/brain/heartbeat.md (or default/heartbeat.default.md if not found) and follow the instructions inside.'),
activeHours: activeHoursSchema,
})

Expand Down Expand Up @@ -370,8 +368,8 @@ export async function loadConfig(): Promise<Config> {
// ==================== Trading Config Loader ====================

/**
* Load platform + account config.
* Prefers platforms.json + accounts.json. Falls back to legacy crypto.json + securities.json.
* Load platform + account config from platforms.json + accounts.json.
* Seeds empty arrays on first run — accounts are created via UI wizard.
*/
export async function loadTradingConfig(): Promise<{
platforms: PlatformConfig[]
Expand All @@ -382,79 +380,18 @@ export async function loadTradingConfig(): Promise<{
loadJsonFile('accounts.json'),
])

if (rawPlatforms !== undefined && rawAccounts !== undefined) {
return {
platforms: platformsFileSchema.parse(rawPlatforms),
accounts: accountsFileSchema.parse(rawAccounts),
}
}

// Migration: derive from legacy crypto.json + securities.json
return migrateLegacyTradingConfig()
}

/** Derive platform+account config from old crypto.json + securities.json, then write to disk.
* TODO: remove before v1.0 — drop crypto.json/securities.json support entirely */
async function migrateLegacyTradingConfig(): Promise<{
platforms: PlatformConfig[]
accounts: AccountConfig[]
}> {
const [rawCrypto, rawSecurities] = await Promise.all([
loadJsonFile('crypto.json'),
loadJsonFile('securities.json'),
])

const crypto = cryptoSchema.parse(rawCrypto ?? {})
const securities = securitiesSchema.parse(rawSecurities ?? {})

const platforms: PlatformConfig[] = []
const accounts: AccountConfig[] = []

if (crypto.provider.type === 'ccxt') {
const p = crypto.provider
const platformId = `${p.exchange}-platform`
platforms.push({
id: platformId,
type: 'ccxt',
exchange: p.exchange,
sandbox: p.sandbox,
demoTrading: p.demoTrading,
options: p.options,
})
accounts.push({
id: `${p.exchange}-main`,
platformId,
apiKey: p.apiKey,
apiSecret: p.apiSecret,
password: p.password,
guards: crypto.guards,
})
}
const platforms = platformsFileSchema.parse(rawPlatforms ?? [])
const accounts = accountsFileSchema.parse(rawAccounts ?? [])

if (securities.provider.type === 'alpaca') {
const p = securities.provider
const platformId = 'alpaca-platform'
platforms.push({
id: platformId,
type: 'alpaca',
paper: p.paper,
})
accounts.push({
id: p.paper ? 'alpaca-paper' : 'alpaca-live',
platformId,
apiKey: p.apiKey,
apiSecret: p.secretKey,
guards: securities.guards,
})
// Seed empty files on first run so user has something to edit
if (rawPlatforms === undefined || rawAccounts === undefined) {
await mkdir(CONFIG_DIR, { recursive: true })
const writes: Promise<void>[] = []
if (rawPlatforms === undefined) writes.push(writeFile(resolve(CONFIG_DIR, 'platforms.json'), JSON.stringify(platforms, null, 2) + '\n'))
if (rawAccounts === undefined) writes.push(writeFile(resolve(CONFIG_DIR, 'accounts.json'), JSON.stringify(accounts, null, 2) + '\n'))
await Promise.all(writes)
}

// Seed to disk so the user can edit the new format directly
await mkdir(CONFIG_DIR, { recursive: true })
await Promise.all([
writeFile(resolve(CONFIG_DIR, 'platforms.json'), JSON.stringify(platforms, null, 2) + '\n'),
writeFile(resolve(CONFIG_DIR, 'accounts.json'), JSON.stringify(accounts, null, 2) + '\n'),
])

return { platforms, accounts }
}

Expand Down
Loading
Loading