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
31 changes: 31 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: CI

on:
pull_request:
push:
branches:
- main

jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest

- name: Install dependencies
run: bun install

- name: Typecheck
run: bun run typecheck

- name: Test
run: bun test

- name: Build
run: bun run build
8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@
"scripts": {
"dev": "concurrently -n server,vite -c blue,magenta \"bun run src/server/index.ts\" \"cd src/frontend && bunx vite dev --port 3030\"",
"build": "bun run scripts/build.ts",
"start": "./admiral"
"start": "./admiral",
"typecheck": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p src/frontend/tsconfig.json",
"test": "bun test"
},
"dependencies": {
"@mariozechner/pi-ai": "latest",
"@mariozechner/pi-ai": "^0.73.1",
"@opentelemetry/api": "^1.9.1",
"@tanstack/react-virtual": "^3.13.18",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
Expand All @@ -24,6 +27,7 @@
"devDependencies": {
"@tailwindcss/postcss": "^4.2.0",
"@tailwindcss/vite": "^4.2.0",
"@types/bun": "^1.3.14",
"@types/node": "^25.3.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
Expand Down
7 changes: 3 additions & 4 deletions src/server/lib/agent-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,9 @@ type SlimGameState = {

function slimGameState(raw: Record<string, unknown> | null): SlimGameState {
if (!raw) return null
const gs = raw as Record<string, Record<string, unknown> & { cargo?: unknown[]; current_ammo?: unknown; magazine_size?: unknown }>
const player = gs.player as Record<string, unknown> | undefined
const ship = gs.ship as Record<string, unknown> & { cargo?: unknown[] } | undefined
const modules = gs.modules as Array<Record<string, unknown>> | undefined
const player = raw.player as Record<string, unknown> | undefined
const ship = raw.ship as (Record<string, unknown> & { cargo?: unknown[] }) | undefined
const modules = raw.modules as Array<Record<string, unknown> & { current_ammo?: unknown; magazine_size?: unknown }> | undefined
return {
credits: player?.credits,
system: player?.current_system,
Expand Down
6 changes: 3 additions & 3 deletions src/server/lib/connections/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ export class HttpConnection implements GameConnection {
})
if (!resp.ok) throw new Error(`Failed to create session: ${resp.status}`)

const data = await resp.json()
const data = await resp.json() as { session?: ApiSession }
if (data.session) {
this.session = data.session
} else {
Expand Down Expand Up @@ -194,9 +194,9 @@ export class HttpConnection implements GameConnection {
}

try {
const data = await resp.json()
const data = await resp.json() as CommandResult & { session?: ApiSession }
if (data.session) this.session = data.session
return data as CommandResult
return data
} catch {
return { error: { code: 'http_error', message: `HTTP ${resp.status}` } }
}
Expand Down
6 changes: 3 additions & 3 deletions src/server/lib/connections/http_v2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ export class HttpV2Connection implements GameConnection {
})
if (!resp.ok) throw new Error(`Failed to create session: ${resp.status}`)

const data = await resp.json()
const data = await resp.json() as { session?: ApiSession }
if (data.session) {
this.session = data.session
} else {
Expand Down Expand Up @@ -280,12 +280,12 @@ export class HttpV2Connection implements GameConnection {
}

try {
const data = await resp.json()
const data = await resp.json() as CommandResult & { session?: ApiSession }
if (data.session) this.session = data.session
// v2 returns { result: <rendered text>, structuredContent: <JSON> }
// Keep both: result (text) goes to the LLM, structuredContent is used
// for cacheGameState and player data display.
return data as CommandResult
return data
} catch {
return { error: { code: 'http_error', message: `HTTP ${resp.status}` } }
}
Expand Down
2 changes: 1 addition & 1 deletion src/server/lib/connections/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ export class McpConnection implements GameConnection {
return { error: { message: 'No matching response in SSE stream' } }
}

return await resp.json()
return await resp.json() as { result?: unknown; error?: { code?: number; message: string } }
}

private async sendNotification(method: string, params: unknown): Promise<void> {
Expand Down
25 changes: 22 additions & 3 deletions src/server/lib/db.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Database } from 'bun:sqlite'
import type { SQLQueryBindings } from 'bun:sqlite'
import path from 'path'
import fs from 'fs'
import type { Provider, Profile, LogEntry } from '../../shared/types'
Expand Down Expand Up @@ -182,7 +183,7 @@ export function updateProfile(id: string, updates: Partial<Profile>): Profile |
sets.push("updated_at = datetime('now')")
vals.push(id)

getDb().query(`UPDATE profiles SET ${sets.join(', ')} WHERE id = ?`).run(...vals)
getDb().query(`UPDATE profiles SET ${sets.join(', ')} WHERE id = ?`).run(...(vals as SQLQueryBindings[]))
return getProfile(id)
}

Expand All @@ -192,11 +193,29 @@ export function deleteProfile(id: string): void {

// --- Log CRUD ---

// Every LLM call, tool call, and server response gets logged here with no
// other cap, so admiral.db grows unbounded over a long-running agent
// (reported: 18GB). Trim old rows past this cap, checked probabilistically
// on insert so we're not running a DELETE on every single log write.
const LOG_ENTRY_CAP_PER_PROFILE = 20_000
const LOG_TRIM_CHECK_INTERVAL = 200

export function addLogEntry(profileId: string, type: string, summary: string, detail?: string): number {
const result = getDb().query(
const db = getDb()
const result = db.query(
'INSERT INTO log_entries (profile_id, type, summary, detail) VALUES (?, ?, ?, ?)'
).run(profileId, type, summary, detail ?? null)
return Number(result.lastInsertRowid)
const id = Number(result.lastInsertRowid)
if (id % LOG_TRIM_CHECK_INTERVAL === 0) trimLogEntries(profileId)
return id
}

function trimLogEntries(profileId: string): void {
getDb().query(
`DELETE FROM log_entries WHERE profile_id = ? AND id NOT IN (
SELECT id FROM log_entries WHERE profile_id = ? ORDER BY id DESC LIMIT ?
)`
).run(profileId, profileId, LOG_ENTRY_CAP_PER_PROFILE)
}

export function getLogEntries(profileId: string, afterId?: number, limit: number = 100): LogEntry[] {
Expand Down
2 changes: 1 addition & 1 deletion src/server/lib/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export async function fetchOpenApiSpec(
}
throw new Error(`HTTP ${resp.status}`)
}
const spec = await resp.json()
const spec = await resp.json() as Record<string, unknown>
// Cache on success
try {
setPreference(cacheKey, JSON.stringify(spec))
Expand Down
2 changes: 1 addition & 1 deletion src/server/routes/logs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ logs.get('/:id/logs', async (c) => {
// Heartbeat - must be well under the Bun.serve idleTimeout (120s)
const heartbeat = setInterval(() => {
if (closed) { clearInterval(heartbeat); return }
stream.writeSSE({ data: '', comment: 'heartbeat' }).catch(() => { closed = true })
stream.writeSSE({ data: '' }).catch(() => { closed = true })
}, 30000)

// Keep the stream open until client disconnects
Expand Down
4 changes: 2 additions & 2 deletions src/server/routes/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ async function fetchOllamaModels(baseUrl?: string): Promise<string[]> {
try {
const resp = await fetch(`${base}/api/tags`, { signal: AbortSignal.timeout(5000) })
if (!resp.ok) return []
const data = await resp.json()
const data = await resp.json() as Record<string, unknown>
const modelList = data.models as { name: string }[] | undefined
return (modelList || []).map(m => m.name).sort()
} catch {
Expand All @@ -105,7 +105,7 @@ async function fetchOpenAICompatModels(apiUrl: string, apiKey?: string): Promise

const resp = await fetch(apiUrl, { headers, signal: AbortSignal.timeout(10000) })
if (!resp.ok) return []
const data = await resp.json()
const data = await resp.json() as Record<string, unknown>
const modelList = data.data as { id: string }[] | undefined
return (modelList || []).map(m => m.id).sort()
} catch {
Expand Down
Loading