repositories
loading repo index
repositories
loading repo index
repository
loading code, commits, and activity
public Clawd ADK gateway launch mirror
stars
latest
clone command
git clone gitlawb://did:key:z6Mkq5mY...iFZ5/my-project-publ...git clone gitlawb://did:key:z6Mkq5mY.../my-project-publ...2fa351d6docs: add automaton and perps launch sources15d ago| #1 | #!/usr/bin/env node |
| #2 | /** |
| #3 | * ooda/tui.ts — Dark ANSI TUI renderer for the OODA loop |
| #4 | * |
| #5 | * Reads JSONL from stdin (loop.ts --tui output) and renders a |
| #6 | * live dark-themed dashboard. Pipe usage: |
| #7 | * |
| #8 | * npx tsx ooda/loop.ts --ticks 200 --sleep 0.4 --tui | npx tsx ooda/tui.ts |
| #9 | * |
| #10 | * Also standalone-importable by the main hermes TUI. |
| #11 | */ |
| #12 | |
| #13 | import { createInterface } from 'node:readline'; |
| #14 | import chalk from 'chalk'; |
| #15 | |
| #16 | // ─── ANSI helpers ───────────────────────────────────────────────────────────── |
| #17 | |
| #18 | const CLEAR = '\x1b[2J\x1b[H'; |
| #19 | const HIDE_CURSOR = '\x1b[?25l'; |
| #20 | const SHOW_CURSOR = '\x1b[?25h'; |
| #21 | |
| #22 | // ─── State ──────────────────────────────────────────────────────────────────── |
| #23 | |
| #24 | interface TickEvent { |
| #25 | event: 'tick' | 'start' | 'done' | 'killswitch'; |
| #26 | tick?: number; |
| #27 | now?: string; |
| #28 | price?: number; |
| #29 | decision?: { action: string; reason: string; side?: string; size_lamports?: number; position_id?: string }; |
| #30 | outcome?: string; |
| #31 | pnl?: number; |
| #32 | total_pnl_lamports?: number; |
| #33 | cash_lamports?: number; |
| #34 | positions?: number; |
| #35 | consecutive_losses?: number; |
| #36 | ticks?: number; |
| #37 | } |
| #38 | |
| #39 | interface DisplayState { |
| #40 | lastTick: number; |
| #41 | totalTicks: number; |
| #42 | price: number; |
| #43 | priceHistory: number[]; |
| #44 | lastDecision: TickEvent['decision'] | null; |
| #45 | lastOutcome: string; |
| #46 | totalPnl: number; |
| #47 | cash: number; |
| #48 | openPositions: number; |
| #49 | consecutiveLosses: number; |
| #50 | log: string[]; |
| #51 | done: boolean; |
| #52 | killswitch: boolean; |
| #53 | } |
| #54 | |
| #55 | const ds: DisplayState = { |
| #56 | lastTick: 0, |
| #57 | totalTicks: 0, |
| #58 | price: 0, |
| #59 | priceHistory: [], |
| #60 | lastDecision: null, |
| #61 | lastOutcome: '', |
| #62 | totalPnl: 0, |
| #63 | cash: 0, |
| #64 | openPositions: 0, |
| #65 | consecutiveLosses: 0, |
| #66 | log: [], |
| #67 | done: false, |
| #68 | killswitch: false, |
| #69 | }; |
| #70 | |
| #71 | // ─── Sparkline ──────────────────────────────────────────────────────────────── |
| #72 | |
| #73 | const SPARK = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█']; |
| #74 | |
| #75 | function sparkline(prices: number[], width = 30): string { |
| #76 | if (prices.length < 2) return chalk.gray('·'.repeat(width)); |
| #77 | const slice = prices.slice(-width); |
| #78 | const min = Math.min(...slice); |
| #79 | const max = Math.max(...slice); |
| #80 | const range = max - min || 1; |
| #81 | return slice |
| #82 | .map(p => { |
| #83 | const idx = Math.round(((p - min) / range) * (SPARK.length - 1)); |
| #84 | const bar = SPARK[idx] ?? '▄'; |
| #85 | const isUp = p > prices[prices.indexOf(p) - 1]; |
| #86 | return isUp ? chalk.green(bar) : chalk.red(bar); |
| #87 | }) |
| #88 | .join(''); |
| #89 | } |
| #90 | |
| #91 | // ─── Render ─────────────────────────────────────────────────────────────────── |
| #92 | |
| #93 | function pnlColor(n: number): string { |
| #94 | const s = (n >= 0 ? '+' : '') + n.toLocaleString() + ' lamports'; |
| #95 | return n >= 0 ? chalk.green(s) : chalk.red(s); |
| #96 | } |
| #97 | |
| #98 | function render(): void { |
| #99 | const lines: string[] = []; |
| #100 | const w = process.stdout.columns ?? 100; |
| #101 | const border = chalk.magenta('═'.repeat(w)); |
| #102 | |
| #103 | // Header |
| #104 | lines.push(chalk.magenta('╔') + border + chalk.magenta('╗')); |
| #105 | const title = ' 🦞 DARK RALPH — OODA Paper Loop · devnet · paper '; |
| #106 | const titlePad = Math.max(0, w - title.length); |
| #107 | lines.push(chalk.magenta('║') + chalk.bold.magenta(title) + ' '.repeat(titlePad) + chalk.magenta('║')); |
| #108 | lines.push(chalk.magenta('╠') + border + chalk.magenta('╣')); |
| #109 | |
| #110 | // Progress bar |
| #111 | const pct = ds.totalTicks > 0 ? ds.lastTick / ds.totalTicks : 0; |
| #112 | const barW = Math.max(10, w - 20); |
| #113 | const filled = Math.round(pct * barW); |
| #114 | const bar = chalk.cyan('█'.repeat(filled)) + chalk.gray('░'.repeat(barW - filled)); |
| #115 | const pctStr = ` Tick ${ds.lastTick}/${ds.totalTicks} [${bar}] ${Math.round(pct * 100)}%`; |
| #116 | const pctPad = Math.max(0, w - pctStr.replace(/\x1b\[[0-9;]*m/g, '').length); |
| #117 | lines.push(chalk.magenta('║') + pctStr + ' '.repeat(pctPad) + chalk.magenta('║')); |
| #118 | |
| #119 | // Price + sparkline |
| #120 | const priceStr = ds.price > 0 ? `$${(ds.price / 1000).toFixed(3)}` : '---'; |
| #121 | const spark = sparkline(ds.priceHistory, Math.min(40, w - 30)); |
| #122 | const priceRow = ` SOL ~${chalk.yellow.bold(priceStr)} ${spark}`; |
| #123 | const priceRowPlain = ` SOL ~${priceStr} ` + '·'.repeat(Math.min(40, w - 30)); |
| #124 | const pricePad = Math.max(0, w - priceRowPlain.length); |
| #125 | lines.push(chalk.magenta('║') + priceRow + ' '.repeat(pricePad) + chalk.magenta('║')); |
| #126 | |
| #127 | // Decision |
| #128 | const dec = ds.lastDecision; |
| #129 | let decStr = ' [--] waiting…'; |
| #130 | if (dec) { |
| #131 | const actionColor = dec.action === 'open' ? chalk.green : |
| #132 | dec.action === 'close' ? chalk.red : chalk.gray; |
| #133 | const outcomeColor = ds.lastOutcome === 'rejected' ? chalk.red : |
| #134 | ds.lastOutcome === 'killswitch' ? chalk.bgRed.white : chalk.cyan; |
| #135 | decStr = ` [${actionColor(dec.action.toUpperCase())}] ${chalk.white(dec.reason?.slice(0, 70) ?? '')} ${outcomeColor(ds.lastOutcome)}`; |
| #136 | } |
| #137 | const decPlain = ` [${dec?.action?.toUpperCase() ?? '--'}] ${dec?.reason?.slice(0, 70) ?? ''} ${ds.lastOutcome}`; |
| #138 | const decPad = Math.max(0, w - decPlain.length); |
| #139 | lines.push(chalk.magenta('║') + decStr + ' '.repeat(decPad) + chalk.magenta('║')); |
| #140 | |
| #141 | lines.push(chalk.magenta('╠') + border + chalk.magenta('╣')); |
| #142 | |
| #143 | // Stats row |
| #144 | const statsRow = [ |
| #145 | ` PnL: ${pnlColor(ds.totalPnl)}`, |
| #146 | `Cash: ${chalk.cyan(ds.cash.toLocaleString())} lam`, |
| #147 | `Pos: ${chalk.yellow(ds.openPositions)}`, |
| #148 | `Losses: ${ds.consecutiveLosses > 0 ? chalk.red(ds.consecutiveLosses) : chalk.gray('0')}`, |
| #149 | ].join(' · '); |
| #150 | const statsPlain = ` PnL: ${ds.totalPnl >= 0 ? '+' : ''}${ds.totalPnl} lamports · Cash: ${ds.cash} lam · Pos: ${ds.openPositions} · Losses: ${ds.consecutiveLosses}`; |
| #151 | const statsPad = Math.max(0, w - statsPlain.length); |
| #152 | lines.push(chalk.magenta('║') + statsRow + ' '.repeat(statsPad) + chalk.magenta('║')); |
| #153 | |
| #154 | // Log |
| #155 | lines.push(chalk.magenta('╠') + border + chalk.magenta('╣')); |
| #156 | const logLines = ds.log.slice(-6); |
| #157 | for (const entry of logLines) { |
| #158 | const pad = Math.max(0, w - entry.replace(/\x1b\[[0-9;]*m/g, '').length); |
| #159 | lines.push(chalk.magenta('║') + entry + ' '.repeat(pad) + chalk.magenta('║')); |
| #160 | } |
| #161 | |
| #162 | // Footer |
| #163 | if (ds.done) { |
| #164 | lines.push(chalk.magenta('╠') + border + chalk.magenta('╣')); |
| #165 | const doneMsg = ds.killswitch |
| #166 | ? ' ⛔ KILLSWITCH TRIGGERED — consecutive losses limit reached' |
| #167 | : ' ✅ Loop complete — see ooda/journal/ticks.jsonl for full log'; |
| #168 | const donePad = Math.max(0, w - doneMsg.length); |
| #169 | lines.push(chalk.magenta('║') + chalk.bold(ds.killswitch ? chalk.red(doneMsg) : chalk.green(doneMsg)) + ' '.repeat(donePad) + chalk.magenta('║')); |
| #170 | } |
| #171 | lines.push(chalk.magenta('╚') + border + chalk.magenta('╝')); |
| #172 | |
| #173 | process.stdout.write(CLEAR + lines.join('\n') + '\n'); |
| #174 | } |
| #175 | |
| #176 | // ─── Main ───────────────────────────────────────────────────────────────────── |
| #177 | |
| #178 | process.stdout.write(HIDE_CURSOR); |
| #179 | process.on('exit', () => process.stdout.write(SHOW_CURSOR)); |
| #180 | process.on('SIGINT', () => { process.stdout.write(SHOW_CURSOR); process.exit(0); }); |
| #181 | |
| #182 | const rl = createInterface({ input: process.stdin }); |
| #183 | |
| #184 | rl.on('line', (line: string) => { |
| #185 | if (!line.trim()) return; |
| #186 | try { |
| #187 | const ev = JSON.parse(line) as TickEvent; |
| #188 | |
| #189 | if (ev.event === 'start') { |
| #190 | ds.totalTicks = ev.ticks ?? 50; |
| #191 | } else if (ev.event === 'tick') { |
| #192 | ds.lastTick = ev.tick ?? ds.lastTick; |
| #193 | ds.price = ev.price ?? ds.price; |
| #194 | ds.priceHistory.push(ds.price); |
| #195 | if (ds.priceHistory.length > 60) ds.priceHistory.shift(); |
| #196 | ds.lastDecision = ev.decision ?? ds.lastDecision; |
| #197 | ds.lastOutcome = ev.outcome ?? ''; |
| #198 | ds.totalPnl = ev.total_pnl_lamports ?? ds.totalPnl; |
| #199 | ds.cash = ev.cash_lamports ?? ds.cash; |
| #200 | ds.openPositions = ev.positions ?? ds.openPositions; |
| #201 | ds.consecutiveLosses = ev.consecutive_losses ?? ds.consecutiveLosses; |
| #202 | |
| #203 | const action = ev.decision?.action ?? 'hold'; |
| #204 | const actionColor = action === 'open' ? chalk.green : action === 'close' ? chalk.red : chalk.gray; |
| #205 | ds.log.push( |
| #206 | ` ${chalk.gray(new Date(ev.now ?? '').toTimeString().slice(0, 8))} ` + |
| #207 | `[${chalk.yellow('T' + ev.tick)}] ` + |
| #208 | `${actionColor(action.toUpperCase().padEnd(5))} ` + |
| #209 | chalk.white((ev.decision?.reason ?? '').slice(0, 55)), |
| #210 | ); |
| #211 | } else if (ev.event === 'killswitch') { |
| #212 | ds.killswitch = true; |
| #213 | ds.done = true; |
| #214 | } else if (ev.event === 'done') { |
| #215 | ds.done = true; |
| #216 | } |
| #217 | |
| #218 | render(); |
| #219 | } catch { /* skip non-JSON */ } |
| #220 | }); |
| #221 | |
| #222 | rl.on('close', () => { |
| #223 | ds.done = true; |
| #224 | render(); |
| #225 | process.stdout.write(SHOW_CURSOR); |
| #226 | }); |
| #227 |