From f95288587075a8fe04739be2095027cd4820c391 Mon Sep 17 00:00:00 2001 From: Mat Milbury Date: Mon, 27 Apr 2026 10:49:48 +0200 Subject: [PATCH 1/5] fix: style OAuth callback page to match ai-auth design Replace bare HTML with styled page using black background, JetBrains Mono font, purple accent (#a299ff), and subtle card borders consistent with the ai-auth app aesthetic. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/auth/localhost-flow.ts | 51 +++++++++++++++++++++++++++++++------- 1 file changed, 42 insertions(+), 9 deletions(-) diff --git a/src/auth/localhost-flow.ts b/src/auth/localhost-flow.ts index f1a8895..325cf24 100644 --- a/src/auth/localhost-flow.ts +++ b/src/auth/localhost-flow.ts @@ -12,6 +12,38 @@ const CALLBACK_PORT = 9876; const REDIRECT_URI = `http://127.0.0.1:${CALLBACK_PORT}/callback`; const SCOPE = "mcp:full"; +function callbackPage(title: string, message: string, success: boolean): string { + const iconColor = success ? "#a299ff" : "#f87171"; + const icon = success ? "✓" : "✗"; + return ` + +${title} — Eterna CLI + + + +
+ ${icon} +

${title}

+

${message}

+
Eterna
+
`; +} + export function generateCodeVerifier(): string { return randomBytes(32).toString("base64url"); } @@ -73,9 +105,8 @@ export async function localhostAuthFlow( if (error) { res.writeHead(200, { "Content-Type": "text/html" }); - res.end( - "

Authorization Failed

You can close this window.

", - ); + res.end(callbackPage("Authorization Failed", "Something went wrong. You can close this window.", false)); + server.closeAllConnections(); server.close(); reject(new Error(`OAuth error: ${error}`)); return; @@ -83,17 +114,18 @@ export async function localhostAuthFlow( if (!code || !receivedState) { res.writeHead(400, { "Content-Type": "text/html" }); - res.end("

Missing parameters

"); + res.end(callbackPage("Missing Parameters", "The callback was missing required parameters.", false)); + server.closeAllConnections(); server.close(); reject(new Error("Missing code or state in callback")); return; } res.writeHead(200, { "Content-Type": "text/html" }); - res.end( - "

Authorized!

You can close this window and return to your terminal.

", - ); - server.close(); + res.end(callbackPage("Authorized!", "You can close this window and return to your terminal.", true), () => { + server.closeAllConnections(); + server.close(); + }); resolve({ code, receivedState }); } }); @@ -106,10 +138,11 @@ export async function localhostAuthFlow( }); }); - setTimeout(() => { + const timeout = setTimeout(() => { server.close(); reject(new Error("Authorization timed out after 5 minutes")); }, 300_000); + timeout.unref(); }); if (receivedState !== state) { From 40af5da90c844d3ef79ed98a98ebeb9e8b9381e7 Mon Sep 17 00:00:00 2001 From: Mat Milbury Date: Mon, 27 Apr 2026 10:49:53 +0200 Subject: [PATCH 2/5] feat: add `eterna status` command to show auth state Shows authentication status, configured endpoint, and token expiry with remaining time in minutes. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/cli.ts | 2 ++ src/commands/status.ts | 29 +++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 src/commands/status.ts diff --git a/src/cli.ts b/src/cli.ts index f78dd05..fe8df1c 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -5,6 +5,7 @@ import { executeCommand } from "./commands/execute.js"; import { sdkCommand } from "./commands/sdk.js"; import { balanceCommand } from "./commands/balance.js"; import { positionsCommand } from "./commands/positions.js"; +import { statusCommand } from "./commands/status.js"; const program = new Command(); @@ -15,6 +16,7 @@ program program.addCommand(loginCommand); program.addCommand(logoutCommand); +program.addCommand(statusCommand); program.addCommand(executeCommand); program.addCommand(sdkCommand); program.addCommand(balanceCommand); diff --git a/src/commands/status.ts b/src/commands/status.ts new file mode 100644 index 0000000..552b9c6 --- /dev/null +++ b/src/commands/status.ts @@ -0,0 +1,29 @@ +import { Command } from "commander"; +import { TokenManager } from "../auth/token-manager.js"; +import { readCredentials, readConfig } from "../auth/config.js"; + +export const statusCommand = new Command("status") + .description("Show authentication status") + .action(async () => { + const tokenManager = new TokenManager(); + const creds = readCredentials(); + + if (!creds || !tokenManager.isAuthenticated()) { + console.log("Not authenticated. Run `eterna login` to get started."); + return; + } + + const config = readConfig(); + const expired = tokenManager.needsRefresh(); + + console.log(`Authenticated`); + console.log(` Endpoint: ${config.endpoint}`); + + const expiresAt = new Date(creds.expiresAt); + if (expired) { + console.log(` Token: expired (${expiresAt.toLocaleString()})`); + } else { + const remaining = Math.round((creds.expiresAt - Date.now()) / 60_000); + console.log(` Token: valid (expires in ${remaining}m)`); + } + }); From 4c1e2ca218430a0d66cdae428c886b0fe8aac527 Mon Sep 17 00:00:00 2001 From: Mat Milbury Date: Mon, 27 Apr 2026 10:50:03 +0200 Subject: [PATCH 3/5] fix: parse JSON error bodies in execute API failures Extract error/message/detail fields from JSON responses instead of showing raw status text, giving users actionable error information. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/api/client.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/api/client.ts b/src/api/client.ts index 40a8483..4393bad 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -62,14 +62,25 @@ export class ApiClient { body: JSON.stringify({ code }), }); if (!retryRes.ok) { - throw new Error(`Execution failed: ${retryRes.statusText}`); + const errBody = await retryRes.text(); + throw new Error(`Execution failed (${retryRes.status}): ${errBody}`); } return retryRes.json() as Promise; } if (!res.ok) { const errBody = await res.text(); - throw new Error(`Execution failed (${res.status}): ${errBody}`); + let detail = errBody; + try { + const parsed = JSON.parse(errBody) as Record; + detail = (parsed.error as string) + ?? (parsed.message as string) + ?? (parsed.detail as string) + ?? errBody; + } catch { /* use raw body */ } + throw new Error( + `Execution failed (${res.status}): ${detail}`, + ); } return res.json() as Promise; From 9b9675d2a031d70144469c364b9cb597839b5301 Mon Sep 17 00:00:00 2001 From: Mat Milbury Date: Mon, 27 Apr 2026 10:50:09 +0200 Subject: [PATCH 4/5] feat: add inline code execution via `-e`/`--eval` flag Allows running code directly without a file: eterna execute -e 'await eterna.getBalance()' Co-Authored-By: Claude Opus 4.6 (1M context) --- src/commands/execute.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/commands/execute.ts b/src/commands/execute.ts index c103f04..a217444 100644 --- a/src/commands/execute.ts +++ b/src/commands/execute.ts @@ -22,15 +22,20 @@ async function readStdin(): Promise { export const executeCommand = new Command("execute") .description("Execute trading code in the Eterna sandbox") .argument("[file]", "TypeScript file to execute, or - for stdin") - .action(async (file?: string) => { + .option("-e, --eval ", "Execute inline code instead of a file") + .action(async (file: string | undefined, opts: { eval?: string }) => { let code: string; - if (file === "-" || (!file && !process.stdin.isTTY)) { + if (opts.eval) { + code = opts.eval; + } else if (file === "-" || (!file && !process.stdin.isTTY)) { code = await readStdin(); } else if (file) { code = await readCodeFromFile(file); } else { - console.error("Usage: eterna execute or pipe code via stdin"); + console.error( + "Usage: eterna execute , eterna execute -e '', or pipe via stdin", + ); process.exitCode = 1; return; } From b06b2b355ed1cf16dca053ec02ee30e96ec430c6 Mon Sep 17 00:00:00 2001 From: Mat Milbury Date: Mon, 27 Apr 2026 10:50:13 +0200 Subject: [PATCH 5/5] chore: add CLAUDE.md with conventional commits guidance Co-Authored-By: Claude Opus 4.6 (1M context) --- CLAUDE.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..4bc42f5 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,18 @@ +# Eterna CLI + +## Releases + +This repo uses [Release Please](https://github.com/googleapis/release-please) for automated releases and changelog generation. + +**Always use [Conventional Commits](https://www.conventionalcommits.org/)** so Release Please can determine the correct semantic version bump: + +- `fix:` → patch (0.0.X) +- `feat:` → minor (0.X.0) +- `feat!:` or `BREAKING CHANGE:` → major (X.0.0) + +Examples: +``` +fix: parse JSON error bodies in execute failures +feat: add `eterna status` command +feat: add inline code execution via `-e` flag +```