Skip to content
Open
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
6 changes: 5 additions & 1 deletion bin/cli.mjs
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,11 @@ Usage: npx @blockrun/clawrouter-codex <command>
@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 <command>
Machine output: wallet/doctor accept --json (BlockRun {ok,data|error} contract)`);
}

if (!cmd || cmd === "help" || cmd === "-h" || cmd === "--help") {
Expand Down
11 changes: 11 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -49,6 +50,7 @@
],
"dependencies": {
"@blockrun/clawrouter": "^0.12.212",
"@blockrun/core": "^0.0.3",
"@blockrun/llm": "^3.5.0",
"viem": "^2.53.1"
}
Expand Down
19 changes: 17 additions & 2 deletions scripts/doctor.mjs
Original file line number Diff line number Diff line change
@@ -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}`);
}
Expand All @@ -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;
Expand Down Expand Up @@ -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);
10 changes: 6 additions & 4 deletions scripts/start.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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(); }

Expand Down
46 changes: 28 additions & 18 deletions scripts/wallet.mjs
Original file line number Diff line number Diff line change
@@ -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`, {
Expand All @@ -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);
}
28 changes: 14 additions & 14 deletions src/direct.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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) {
Expand All @@ -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;
}
Expand Down
9 changes: 7 additions & 2 deletions src/wallet.js
Original file line number Diff line number Diff line change
@@ -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;
}