diff --git a/bin/cli.mjs b/bin/cli.mjs old mode 100644 new mode 100755 index 764fa1a..75899d1 --- a/bin/cli.mjs +++ b/bin/cli.mjs @@ -50,7 +50,11 @@ Usage: npx @blockrun/clawrouter-codex @blockrun/llm SDK, no proxy (decoupled mode) Quick start: npx @blockrun/clawrouter-codex up - codex --profile clawrouter`); + codex --profile clawrouter + +Also available via the BlockRun umbrella CLI (same wallet, same commands): + blockrun codex +Machine output: wallet/doctor accept --json (BlockRun {ok,data|error} contract)`); } if (!cmd || cmd === "help" || cmd === "-h" || cmd === "--help") { diff --git a/package-lock.json b/package-lock.json index a4861db..5820727 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,10 +10,12 @@ "license": "MIT", "dependencies": { "@blockrun/clawrouter": "^0.12.212", + "@blockrun/core": "^0.0.3", "@blockrun/llm": "^3.5.0", "viem": "^2.53.1" }, "bin": { + "blockrun-codex": "bin/cli.mjs", "clawrouter-codex": "bin/cli.mjs" }, "engines": { @@ -83,6 +85,15 @@ } } }, + "node_modules/@blockrun/core": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@blockrun/core/-/core-0.0.3.tgz", + "integrity": "sha512-w/P0VnhUA6gV/qkQJzKdQNovh5Ne+1+lqs8sTNr4OOLyrjDc2i711fkN/WNwOW9cZSBbHvgV+AI1/leW7+I3lA==", + "license": "MIT", + "dependencies": { + "viem": "^2.21.0" + } + }, "node_modules/@blockrun/llm": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/@blockrun/llm/-/llm-3.5.0.tgz", diff --git a/package.json b/package.json index 77a917b..c16ec3e 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "type": "module", "license": "MIT", "bin": { - "clawrouter-codex": "bin/cli.mjs" + "clawrouter-codex": "bin/cli.mjs", + "blockrun-codex": "bin/cli.mjs" }, "exports": { ".": "./src/server.js", @@ -49,6 +50,7 @@ ], "dependencies": { "@blockrun/clawrouter": "^0.12.212", + "@blockrun/core": "^0.0.3", "@blockrun/llm": "^3.5.0", "viem": "^2.53.1" } diff --git a/scripts/doctor.mjs b/scripts/doctor.mjs index 088a11f..5742fe8 100644 --- a/scripts/doctor.mjs +++ b/scripts/doctor.mjs @@ -1,20 +1,29 @@ #!/usr/bin/env node // doctor — verify the Codex↔ClawRouter link end to end and point at whatever's // missing. Read-only. +// +// `--json` emits the BlockRun output contract from @blockrun/core +// ({ok,data:{checks,fails}}) so the umbrella `blockrun` CLI and agents can +// parse it; human output is unchanged. import { existsSync, readFileSync } from "node:fs"; import { homedir } from "node:os"; import { join } from "node:path"; +import { ok as okEnv, err as errEnv, render } from "@blockrun/core"; const PORT = process.env.PORT ?? "8403"; const CODEX = join(homedir(), ".codex"); const BASE = join(CODEX, "config.toml"); const PROFILE = join(CODEX, "clawrouter.config.toml"); const CATALOG = join(CODEX, "clawrouter-catalog.json"); +const json = process.argv.includes("--json"); let fails = 0; +const checks = []; function line(ok, label, note, hint) { if (!ok) fails++; + checks.push({ ok, label, ...(note ? { note } : {}), ...(!ok && hint ? { hint } : {}) }); + if (json) return; console.log(` ${ok ? "✅" : "❌"} ${label}${note ? ` — ${note}` : ""}`); if (!ok && hint) console.log(` ↳ ${hint}`); } @@ -25,7 +34,7 @@ async function get(url) { } const read = (f) => (existsSync(f) ? readFileSync(f, "utf8") : ""); -console.log("\nclawrouter-codex doctor\n"); +if (!json) console.log("\nclawrouter-codex doctor\n"); // 1. bridge + mode let direct = true; @@ -66,5 +75,11 @@ if (/\[profiles\.clawrouter\]/.test(read(BASE))) { "remove it — Codex v2 uses the separate clawrouter.config.toml"); } -console.log(`\n${fails === 0 ? "✅ all good — `codex --profile clawrouter`" : `❌ ${fails} issue(s) above`}\n`); +if (json) { + const env = fails === 0 ? okEnv({ checks, fails }) : errEnv("doctor", `${fails} issue(s)`, undefined); + // Attach the checks to failures too, so agents always see the full picture. + console.log(render(fails === 0 ? env : { ...env, error: { ...env.error, checks } }, "json")); +} else { + console.log(`\n${fails === 0 ? "✅ all good — `codex --profile clawrouter`" : `❌ ${fails} issue(s) above`}\n`); +} process.exit(fails === 0 ? 0 : 1); diff --git a/scripts/start.mjs b/scripts/start.mjs index c84dd32..63c4290 100644 --- a/scripts/start.mjs +++ b/scripts/start.mjs @@ -21,6 +21,7 @@ import { readFileSync, existsSync, mkdirSync } from "node:fs"; import { homedir } from "node:os"; import { join, dirname } from "node:path"; import { fileURLToPath } from "node:url"; +import { paths as corePaths } from "@blockrun/core"; const ROOT = dirname(dirname(fileURLToPath(import.meta.url))); const PORT = Number(process.env.PORT ?? 8403); @@ -48,10 +49,11 @@ async function waitHealthy(port, label, tries = 40) { return false; } -// Canonical local BlockRun wallet (~/.blockrun/.session). Lives OUTSIDE -// ~/.openclaw, so when we use it we also isolate HOME so ClawRouter's saved -// ~/.openclaw wallet can't shadow it. -const BLOCKRUN_WALLET = join(homedir(), ".blockrun", ".session"); +// Canonical local BlockRun wallet (~/.blockrun/.session), resolved by +// @blockrun/core so the path stays in lock-step with every other product. +// Lives OUTSIDE ~/.openclaw, so when we use it we also isolate HOME so +// ClawRouter's saved ~/.openclaw wallet can't shadow it. +const BLOCKRUN_WALLET = corePaths().session; function rd(p) { return readFileSync(p.replace(/^~/, homedir()), "utf8").trim(); } diff --git a/scripts/wallet.mjs b/scripts/wallet.mjs index a64e1c8..50fa19e 100644 --- a/scripts/wallet.mjs +++ b/scripts/wallet.mjs @@ -1,12 +1,22 @@ #!/usr/bin/env node // wallet — print the full funding wallet address without requiring the dashboard. +// +// `--json` emits the BlockRun output contract from @blockrun/core +// ({ok,data}/{ok,error}) so agents and the umbrella `blockrun` CLI can parse +// every product the same way. import { resolveWalletKey } from "../src/direct.js"; import { walletAddressFromKey } from "../src/wallet.js"; +import { ok, err, render } from "@blockrun/core"; const PORT = process.env.PORT ?? "8403"; const json = process.argv.includes("--json"); +const emit = (env) => { + if (json) console.log(render(env, "json")); + process.exit(env.ok ? 0 : 1); +}; + async function bridgeWallet() { try { const res = await fetch(`http://127.0.0.1:${PORT}/health?full=true`, { @@ -22,30 +32,30 @@ async function bridgeWallet() { try { const key = resolveWalletKey(); if (!key) { - const message = "No BlockRun wallet found. Start the bridge with `npx @blockrun/clawrouter-codex up` or set BLOCKRUN_WALLET_KEY."; - if (json) console.log(JSON.stringify({ ok: false, error: message }, null, 2)); - else console.error(message); + const message = + "No BlockRun wallet found. Start the bridge with `npx @blockrun/clawrouter-codex up` or set BLOCKRUN_WALLET_KEY."; + if (json) emit(err("wallet", message, 404)); + console.error(message); process.exit(1); } const address = walletAddressFromKey(key); const health = await bridgeWallet(); - const balance = typeof health.balance === "string" || typeof health.balance === "number" ? String(health.balance) : undefined; + const balance = + typeof health.balance === "string" || typeof health.balance === "number" ? String(health.balance) : undefined; const dashboard = `http://127.0.0.1:${PORT}/dashboard`; - if (json) { - console.log(JSON.stringify({ ok: true, address, balance, dashboard }, null, 2)); - } else { - console.log("BlockRun wallet"); - console.log(`Address: ${address}`); - console.log(`Balance: ${balance ?? `unavailable (start the bridge for live balance)`}`); - console.log(`Dashboard: ${dashboard}`); - console.log(""); - console.log("Fund with USDC on Base."); - } -} catch (err) { - const message = err instanceof Error ? err.message : String(err); - if (json) console.log(JSON.stringify({ ok: false, error: message }, null, 2)); - else console.error(`wallet: ${message}`); + if (json) emit(ok({ address, balance, dashboard })); + + console.log("BlockRun wallet"); + console.log(`Address: ${address}`); + console.log(`Balance: ${balance ?? `unavailable (start the bridge for live balance)`}`); + console.log(`Dashboard: ${dashboard}`); + console.log(""); + console.log("Fund with USDC on Base."); +} catch (e) { + const message = e instanceof Error ? e.message : String(e); + if (json) emit(err("wallet", message)); + console.error(`wallet: ${message}`); process.exit(1); } diff --git a/src/direct.js b/src/direct.js index d7c5176..40c4be5 100644 --- a/src/direct.js +++ b/src/direct.js @@ -10,9 +10,8 @@ // Codex → bridge (translate) → @blockrun/llm → blockrun.ai (no proxy) import { readFileSync } from "node:fs"; -import { homedir } from "node:os"; -import { join } from "node:path"; import { LLMClient, SearchClient } from "@blockrun/llm"; +import { resolvePrivateKey, paths } from "@blockrun/core"; const DEFAULT_API = process.env.BLOCKRUN_API_URL ?? "https://blockrun.ai/api"; // Fallback model when smart routing is unavailable. Override w/ BLOCKRUN_DEFAULT_MODEL. @@ -64,18 +63,17 @@ async function resolveRouting(model, messages, maxTokens, hasTools) { return { model: DEFAULT_MODEL }; } -/** Resolve the EVM wallet key: explicit env → ~/.blockrun/.session (raw 0x key). */ +/** + * Resolve the EVM wallet key: explicit env → ~/.blockrun/.session. + * + * Delegated to `@blockrun/core` so the resolution order (env + * BLOCKRUN_WALLET_KEY|BASE_CHAIN_WALLET_KEY → ~/.blockrun/.session → legacy + * wallet.key) is the single source of truth shared with the SDK, the umbrella + * `blockrun` CLI, and every other BlockRun product. Returns undefined when no + * wallet exists (the SDK then surfaces a funding error on first paid call). + */ export function resolveWalletKey() { - const env = process.env.BLOCKRUN_WALLET_KEY ?? process.env.BASE_CHAIN_WALLET_KEY; - if (env && env.trim()) return env.trim(); - try { - const raw = readFileSync(join(homedir(), ".blockrun", ".session"), "utf8"); - const m = raw.match(/0x[0-9a-fA-F]{64}/); - if (m) return m[0]; - } catch { - /* no local wallet — SDK will surface a funding error on first paid call */ - } - return undefined; + return resolvePrivateKey()?.privateKey; } function json(obj, status = 200) { @@ -98,7 +96,9 @@ function buildStats() { const empty = { days: 7, totalRequests: 0, totalCost: 0, dailyBreakdown: [], byModel: {} }; let raw; try { - raw = readFileSync(join(homedir(), ".blockrun", "cost_log.jsonl"), "utf8"); + // Path from @blockrun/core so BLOCKRUN_HOME isolation and any future layout + // change apply here too (previously hand-joined against os.homedir()). + raw = readFileSync(paths().costLog, "utf8"); } catch { return empty; } diff --git a/src/wallet.js b/src/wallet.js index 73acc7a..77ff864 100644 --- a/src/wallet.js +++ b/src/wallet.js @@ -1,8 +1,13 @@ -import { privateKeyToAccount } from "viem/accounts"; +// Address derivation — delegated to @blockrun/core so every BlockRun product +// derives addresses the same way. This wrapper keeps the strict throw-on-invalid +// contract the bridge (and its tests) rely on. +import { addressFromKey } from "@blockrun/core"; export function walletAddressFromKey(key) { if (typeof key !== "string" || !/^0x[0-9a-fA-F]{64}$/.test(key.trim())) { throw new Error("wallet key must be a 0x-prefixed 32-byte private key"); } - return privateKeyToAccount(key.trim()).address; + const address = addressFromKey(key.trim()); + if (!address) throw new Error("wallet key must be a 0x-prefixed 32-byte private key"); + return address; }