From 47b19d4f1701a2a5be83ccb6e9742eb278880e6a Mon Sep 17 00:00:00 2001 From: Ame Date: Mon, 6 Apr 2026 23:11:29 +0800 Subject: [PATCH 01/15] docs: update README badges and project structure Update website link to openalice.ai, add docs badge, and rewrite Project Structure section to match current monorepo layout (packages/, ui/, server/, removed plugins/ and skills/). Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 162 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 85 insertions(+), 77 deletions(-) diff --git a/README.md b/README.md index 3f93fb8a..bb66fd77 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@

- CI · License: AGPL-3.0 · Ask DeepWiki · traderalice.com + CI · License: AGPL-3.0 · openalice.ai · Docs · Ask DeepWiki

# Open Alice @@ -196,85 +196,93 @@ On first run, defaults are auto-copied to the user override path. Edit the user ## Project Structure +Open Alice is a pnpm monorepo with Turborepo build orchestration. + ``` +packages/ +├── ibkr/ # @traderalice/ibkr — IBKR TWS API TypeScript port +└── opentypebb/ # @traderalice/opentypebb — OpenBB platform TS port +ui/ # React frontend (Vite, 13 pages) src/ - main.ts # Composition root — wires everything together - core/ - agent-center.ts # Top-level AI orchestration, owns ProviderRouter - ai-provider-manager.ts # GenerateRouter + StreamableResult + AskOptions - tool-center.ts # Centralized tool registry (Vercel + MCP export) - session.ts # JSONL session store + format converters - compaction.ts # Auto-summarize long context windows - config.ts # Zod-validated config loader (generic account schema with brokerConfig) - ai-config.ts # Runtime AI provider selection - event-log.ts # Append-only JSONL event log - connector-center.ts # ConnectorCenter — push delivery + last-interacted tracking - async-channel.ts # AsyncChannel for streaming provider events to SSE - model-factory.ts # Model instance factory for Vercel AI SDK - media.ts # MediaAttachment extraction - media-store.ts # Media file persistence - types.ts # Plugin, EngineContext interfaces - ai-providers/ - vercel-ai-sdk/ # Vercel AI SDK ToolLoopAgent wrapper - agent-sdk/ # Claude backend (@anthropic-ai/claude-agent-sdk, OAuth + API key) - domain/ - trading/ # Unified multi-account trading, guard pipeline, git-like commits - UnifiedTradingAccount.ts # UTA class — owns broker + git + guards + snapshots - account-manager.ts # UTA lifecycle (init, reconnect, enable/disable) + registry - git-persistence.ts # Git state load/save - brokers/ - registry.ts # Broker self-registration (configSchema + configFields + fromConfig) - alpaca/ # Alpaca (US equities) - ccxt/ # CCXT (100+ crypto exchanges) - ibkr/ # Interactive Brokers (TWS/Gateway) - mock/ # In-memory test broker - git/ # Trading-as-Git engine (stage → commit → push) - guards/ # Pre-execution safety checks (position size, cooldown, whitelist) - snapshot/ # Periodic + event-driven account state capture, equity curve - market-data/ # Structured data layer (typebb in-process + OpenBB API remote) - equity/ # Equity data + SymbolIndex (SEC/TMX local cache) - crypto/ # Crypto data layer - currency/ # Currency/forex data layer - commodity/ # Commodity data layer (EIA, spot prices) - economy/ # Macro economy data layer - client/ # Data backend clients (typebb SDK, openbb-api) - analysis/ # Indicators, technical analysis - news/ # RSS collector + archive search - brain/ # Cognitive state (memory, emotion) - thinking/ # Safe expression evaluator - tool/ # AI tool definitions — thin bridge from domain to ToolCenter - trading.ts # Trading tools (delegates to domain/trading) - equity.ts # Equity fundamental tools (uses domain/market-data) - market.ts # Symbol search tools (uses domain/market-data) - analysis.ts # Indicator calculation tools (uses domain/analysis) - news.ts # News archive tools (uses domain/news) - brain.ts # Cognition tools (uses domain/brain) - thinking.ts # Reasoning tools (uses domain/thinking) - browser.ts # Browser automation tools (wraps openclaw) - server/ - mcp.ts # MCP protocol server - opentypebb.ts # Embedded OpenBB-compatible HTTP API (optional) - connectors/ - web/ # Web UI chat (Hono, SSE streaming, sub-channels) - telegram/ # Telegram bot (grammY, polling, commands) - mcp-ask/ # MCP Ask connector (external agent conversation) - task/ - cron/ # Cron scheduling (engine, listener, AI tools) - heartbeat/ # Periodic heartbeat with structured response protocol - openclaw/ # ⚠️ Frozen — DO NOT MODIFY +├── main.ts # Composition root — wires everything together +├── core/ +│ ├── agent-center.ts # Top-level AI orchestration, owns ProviderRouter +│ ├── ai-provider-manager.ts # GenerateRouter + StreamableResult + AskOptions +│ ├── tool-center.ts # Centralized tool registry (Vercel + MCP export) +│ ├── mcp-export.ts # Shared MCP export layer with type coercion +│ ├── session.ts # JSONL session store + format converters +│ ├── compaction.ts # Auto-summarize long context windows +│ ├── config.ts # Zod-validated config loader +│ ├── event-log.ts # Append-only JSONL event log +│ ├── connector-center.ts # ConnectorCenter — push delivery + last-interacted tracking +│ ├── async-channel.ts # AsyncChannel for streaming provider events to SSE +│ ├── tool-call-log.ts # Tool invocation logging +│ ├── media.ts # MediaAttachment extraction +│ ├── media-store.ts # Media file persistence +│ └── types.ts # Plugin, EngineContext interfaces +├── ai-providers/ +│ ├── vercel-ai-sdk/ # Vercel AI SDK ToolLoopAgent wrapper +│ ├── agent-sdk/ # Claude backend (@anthropic-ai/claude-agent-sdk, OAuth + API key) +│ └── mock/ # Mock provider (testing) +├── domain/ +│ ├── trading/ # Unified multi-account trading, guard pipeline, git-like commits +│ │ ├── account-manager.ts # UTA lifecycle (init, reconnect, enable/disable) + registry +│ │ ├── git-persistence.ts # Git state load/save +│ │ ├── brokers/ +│ │ │ ├── registry.ts # Broker self-registration (configSchema + configFields + fromConfig) +│ │ │ ├── alpaca/ # Alpaca (US equities) +│ │ │ ├── ccxt/ # CCXT (100+ crypto exchanges) +│ │ │ ├── ibkr/ # Interactive Brokers (TWS/Gateway) +│ │ │ └── mock/ # In-memory test broker +│ │ ├── git/ # Trading-as-Git engine (stage → commit → push) +│ │ ├── guards/ # Pre-execution safety checks (position size, cooldown, whitelist) +│ │ └── snapshot/ # Periodic + event-driven account state capture, equity curve +│ ├── market-data/ # Structured data layer (opentypebb in-process + OpenBB API remote) +│ │ ├── equity/ # Equity data + SymbolIndex (SEC/TMX local cache) +│ │ ├── crypto/ # Crypto data layer +│ │ ├── currency/ # Currency/forex data layer +│ │ ├── commodity/ # Commodity data layer (EIA, spot prices) +│ │ ├── economy/ # Macro economy data layer +│ │ └── client/ # Data backend clients (opentypebb SDK, openbb-api) +│ ├── analysis/ # Indicators, technical analysis +│ ├── news/ # RSS collector + archive search +│ ├── brain/ # Cognitive state (memory, emotion) +│ └── thinking/ # Safe expression evaluator +├── tool/ # AI tool definitions — thin bridge from domain to ToolCenter +│ ├── trading.ts # Trading tools (delegates to domain/trading) +│ ├── equity.ts # Equity fundamental tools +│ ├── market.ts # Symbol search tools +│ ├── analysis.ts # Indicator calculation tools +│ ├── news.ts # News archive tools +│ ├── brain.ts # Cognition tools +│ ├── thinking.ts # Reasoning tools +│ ├── browser.ts # Browser automation tools (wraps openclaw) +│ └── session.ts # Session awareness tools +├── server/ +│ ├── mcp.ts # MCP protocol server +│ └── opentypebb.ts # Embedded OpenBB-compatible HTTP API (optional) +├── connectors/ +│ ├── web/ # Web UI (Hono, SSE streaming, sub-channels) +│ ├── telegram/ # Telegram bot (grammY, magic link auth, /trading panel) +│ ├── mcp-ask/ # MCP Ask connector (external agent conversation) +│ └── mock/ # Mock connector (testing) +├── task/ +│ ├── cron/ # Cron scheduling (engine, listener, AI tools) +│ └── heartbeat/ # Periodic heartbeat with structured response protocol +└── openclaw/ # ⚠️ Frozen — DO NOT MODIFY data/ - config/ # JSON configuration files - sessions/ # JSONL conversation histories - brain/ # Agent memory and emotion logs - cache/ # API response caches - trading/ # Trading commit history + snapshots (per-account) - news-collector/ # Persistent news archive (JSONL) - cron/ # Cron job definitions (jobs.json) - event-log/ # Persistent event log (events.jsonl) - tool-calls/ # Tool invocation logs - media/ # Uploaded attachments -default/ # Factory defaults (persona, heartbeat prompts) -docs/ # Architecture documentation +├── config/ # JSON configuration files +├── sessions/ # JSONL conversation histories (web/, telegram/, cron/) +├── brain/ # Agent memory and emotion logs +├── cache/ # API response caches +├── trading/ # Trading commit history + snapshots (per-account) +├── news-collector/ # Persistent news archive (JSONL) +├── cron/ # Cron job definitions (jobs.json) +├── event-log/ # Persistent event log (events.jsonl) +├── tool-calls/ # Tool invocation logs +└── media/ # Uploaded attachments +default/ # Factory defaults (persona, heartbeat, skills) +docs/ # Documentation ``` ## Roadmap to v1 From 0819e7c951c80b54983f09ec9d3d393274dbd910 Mon Sep 17 00:00:00 2001 From: Ame Date: Tue, 7 Apr 2026 10:52:28 +0800 Subject: [PATCH 02/15] fix: persona & heartbeat default file auto-copy + UI editing - Add heartbeat auto-copy at startup (same as persona) - Heartbeat GET route falls back to default/ when data file missing - Add persona GET/PUT backend routes with default fallback - Add persona editor to Settings page in frontend Co-Authored-By: Claude Opus 4.6 (1M context) --- src/connectors/web/routes/heartbeat.ts | 5 ++ src/connectors/web/routes/persona.ts | 40 +++++++++++++ src/connectors/web/web-plugin.ts | 2 + src/main.ts | 3 + ui/src/api/index.ts | 2 + ui/src/api/persona.ts | 18 ++++++ ui/src/pages/SettingsPage.tsx | 80 ++++++++++++++++++++++++++ 7 files changed, 150 insertions(+) create mode 100644 src/connectors/web/routes/persona.ts create mode 100644 ui/src/api/persona.ts diff --git a/src/connectors/web/routes/heartbeat.ts b/src/connectors/web/routes/heartbeat.ts index ec374d48..64090835 100644 --- a/src/connectors/web/routes/heartbeat.ts +++ b/src/connectors/web/routes/heartbeat.ts @@ -4,6 +4,7 @@ import { dirname } from 'node:path' import type { EngineContext } from '../../../core/types.js' const PROMPT_FILE = 'data/brain/heartbeat.md' +const PROMPT_DEFAULT = 'default/heartbeat.default.md' /** Heartbeat routes: GET /status, POST /trigger, PUT /enabled, GET/PUT /prompt-file */ export function createHeartbeatRoutes(ctx: EngineContext) { @@ -43,6 +44,10 @@ export function createHeartbeatRoutes(ctx: EngineContext) { return c.json({ content, path: PROMPT_FILE }) } catch (err: unknown) { if (err instanceof Error && 'code' in err && (err as NodeJS.ErrnoException).code === 'ENOENT') { + try { + const fallback = await readFile(PROMPT_DEFAULT, 'utf-8') + return c.json({ content: fallback, path: PROMPT_FILE }) + } catch { /* default also missing */ } return c.json({ content: '', path: PROMPT_FILE }) } return c.json({ error: String(err) }, 500) diff --git a/src/connectors/web/routes/persona.ts b/src/connectors/web/routes/persona.ts new file mode 100644 index 00000000..eb460829 --- /dev/null +++ b/src/connectors/web/routes/persona.ts @@ -0,0 +1,40 @@ +import { Hono } from 'hono' +import { readFile, writeFile, mkdir } from 'node:fs/promises' +import { dirname } from 'node:path' + +const PERSONA_FILE = 'data/brain/persona.md' +const PERSONA_DEFAULT = 'default/persona.default.md' + +/** Persona routes: GET / (read), PUT / (write) */ +export function createPersonaRoutes() { + const app = new Hono() + + app.get('/', async (c) => { + try { + const content = await readFile(PERSONA_FILE, 'utf-8') + return c.json({ content, path: PERSONA_FILE }) + } catch (err: unknown) { + if (err instanceof Error && 'code' in err && (err as NodeJS.ErrnoException).code === 'ENOENT') { + try { + const fallback = await readFile(PERSONA_DEFAULT, 'utf-8') + return c.json({ content: fallback, path: PERSONA_FILE }) + } catch { /* default also missing */ } + return c.json({ content: '', path: PERSONA_FILE }) + } + return c.json({ error: String(err) }, 500) + } + }) + + app.put('/', async (c) => { + try { + const { content } = await c.req.json<{ content: string }>() + await mkdir(dirname(PERSONA_FILE), { recursive: true }) + await writeFile(PERSONA_FILE, content, 'utf-8') + return c.json({ ok: true }) + } catch (err) { + return c.json({ error: String(err) }, 500) + } + }) + + return app +} diff --git a/src/connectors/web/web-plugin.ts b/src/connectors/web/web-plugin.ts index 4dbdcfdb..0243422e 100644 --- a/src/connectors/web/web-plugin.ts +++ b/src/connectors/web/web-plugin.ts @@ -18,6 +18,7 @@ import { createTradingConfigRoutes } from './routes/trading-config.js' import { createDevRoutes } from './routes/dev.js' import { createToolsRoutes } from './routes/tools.js' import { createAgentStatusRoutes } from './routes/agent-status.js' +import { createPersonaRoutes } from './routes/persona.js' export interface WebConfig { port: number @@ -83,6 +84,7 @@ export class WebPlugin implements Plugin { app.route('/api/dev', createDevRoutes(ctx.connectorCenter)) app.route('/api/tools', createToolsRoutes(ctx.toolCenter)) app.route('/api/agent-status', createAgentStatusRoutes(ctx)) + app.route('/api/persona', createPersonaRoutes()) // ==================== Serve UI (Vite build output) ==================== const uiRoot = resolve('dist/ui') diff --git a/src/main.ts b/src/main.ts index 85d170e2..0f05c3c0 100644 --- a/src/main.ts +++ b/src/main.ts @@ -49,6 +49,8 @@ const FRONTAL_LOBE_FILE = resolve('data/brain/frontal-lobe.md') const EMOTION_LOG_FILE = resolve('data/brain/emotion-log.md') const PERSONA_FILE = resolve('data/brain/persona.md') const PERSONA_DEFAULT = resolve('default/persona.default.md') +const HEARTBEAT_FILE = resolve('data/brain/heartbeat.md') +const HEARTBEAT_DEFAULT = resolve('default/heartbeat.default.md') const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)) @@ -99,6 +101,7 @@ async function main() { const [brainExport, persona] = await Promise.all([ readFile(BRAIN_FILE, 'utf-8').then((r) => JSON.parse(r) as BrainExportState).catch(() => undefined), readWithDefault(PERSONA_FILE, PERSONA_DEFAULT), + readWithDefault(HEARTBEAT_FILE, HEARTBEAT_DEFAULT), ]) const brainDir = resolve('data/brain') diff --git a/ui/src/api/index.ts b/ui/src/api/index.ts index 84fcdbad..94fed442 100644 --- a/ui/src/api/index.ts +++ b/ui/src/api/index.ts @@ -13,6 +13,7 @@ import { devApi } from './dev' import { toolsApi } from './tools' import { channelsApi } from './channels' import { agentStatusApi } from './agentStatus' +import { personaApi } from './persona' export const api = { chat: chatApi, config: configApi, @@ -25,6 +26,7 @@ export const api = { tools: toolsApi, channels: channelsApi, agentStatus: agentStatusApi, + persona: personaApi, } // Re-export all types for convenience diff --git a/ui/src/api/persona.ts b/ui/src/api/persona.ts new file mode 100644 index 00000000..0e665664 --- /dev/null +++ b/ui/src/api/persona.ts @@ -0,0 +1,18 @@ +import { headers } from './client' + +export const personaApi = { + async get(): Promise<{ content: string; path: string }> { + const res = await fetch('/api/persona') + if (!res.ok) throw new Error('Failed to load persona') + return res.json() + }, + + async update(content: string): Promise { + const res = await fetch('/api/persona', { + method: 'PUT', + headers, + body: JSON.stringify({ content }), + }) + if (!res.ok) throw new Error('Failed to save persona') + }, +} diff --git a/ui/src/pages/SettingsPage.tsx b/ui/src/pages/SettingsPage.tsx index f3f43861..cea18ca1 100644 --- a/ui/src/pages/SettingsPage.tsx +++ b/ui/src/pages/SettingsPage.tsx @@ -48,6 +48,11 @@ export function SettingsPage() { + {/* Persona */} + + + + {/* Compaction */} @@ -91,3 +96,78 @@ function CompactionForm({ config }: { config: AppConfig }) { ) } +// ==================== Persona Editor ==================== + +function PersonaEditor() { + const [content, setContent] = useState('') + const [filePath, setFilePath] = useState('') + const [loading, setLoading] = useState(true) + const [saving, setSaving] = useState(false) + const [saved, setSaved] = useState(false) + const [error, setError] = useState(null) + const [dirty, setDirty] = useState(false) + + useEffect(() => { + api.persona.get() + .then(({ content, path }) => { + setContent(content) + setFilePath(path) + }) + .catch(() => setError('Failed to load persona')) + .finally(() => setLoading(false)) + }, []) + + const handleSave = async () => { + setSaving(true) + setError(null) + setSaved(false) + try { + await api.persona.update(content) + setDirty(false) + setSaved(true) + setTimeout(() => setSaved(false), 2000) + } catch { + setError('Failed to save') + } finally { + setSaving(false) + } + } + + if (loading) return
Loading...
+ + return ( + <> +