From 3905be330c325c9dee293ebda1136d4b0dd8578b Mon Sep 17 00:00:00 2001 From: Shahaf Antwarg Date: Wed, 27 May 2026 17:29:24 +0300 Subject: [PATCH 1/2] fix(diem): Preserve withdraw precision --- .../diem-staking/src/components/StakeCard.tsx | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/apps/diem-staking/src/components/StakeCard.tsx b/apps/diem-staking/src/components/StakeCard.tsx index 79ff0c4b..12d14be1 100644 --- a/apps/diem-staking/src/components/StakeCard.tsx +++ b/apps/diem-staking/src/components/StakeCard.tsx @@ -90,7 +90,7 @@ export function StakeCard({ diemPrice, apy }: StakeCardProps) { setTab(next); setAmtEdited(false); if (next === 'stake') setAmt(stakeDefaultAmt); - if (next === 'unstake') setAmt(user.stakedDiem ? fmtDiem(toDiemNumber(user.stakedDiem)) : '0'); + if (next === 'unstake') setAmt(user.stakedDiem ? formatDiemInput(user.stakedDiem) : '0'); }; const diemValue = parseFloat(amt) || 0; @@ -104,7 +104,7 @@ export function StakeCard({ diemPrice, apy }: StakeCardProps) { setAmtEdited(true); if (v === 'max') { if (tab === 'stake' && stakeMaxAmount != null) setAmt(formatDiemInput(stakeMaxAmount)); - else if (tab === 'unstake' && user.stakedDiem != null) setAmt(String(toDiemNumber(user.stakedDiem))); + else if (tab === 'unstake' && user.stakedDiem != null) setAmt(formatDiemInput(user.stakedDiem)); } else { setAmt(v); } @@ -374,6 +374,19 @@ function UnstakePanel(props: UnstakePanelProps) { const { state } = useUnstakeState(); const stakedNum = toDiemNumber(props.stakedDiem); + const unstakeQuickOptions = useMemo(() => { + const staked = props.stakedDiem ?? 0n; + const disabled = props.isConnected && staked === 0n; + const pct = (value: bigint) => formatDiemInput((staked * value) / 100n); + + return [ + { label: '25%', value: pct(25n), disabled }, + { label: '50%', value: pct(50n), disabled }, + { label: '75%', value: pct(75n), disabled }, + { label: 'Max', value: 'max', disabled }, + ]; + }, [props.isConnected, props.stakedDiem]); + let parsedAmt: bigint = 0n; try { parsedAmt = props.amt ? parseEther(props.amt) : 0n; @@ -415,15 +428,7 @@ function UnstakePanel(props: UnstakePanelProps) { setAmt={props.setAmt} amtUsd={props.amtUsd} /> - + {props.isConnected ? ( + ))} ); From c2a6ff09c6c7e5ed7f671205652b5aba533822c3 Mon Sep 17 00:00:00 2001 From: Gabriel Poca Date: Thu, 28 May 2026 18:36:46 +0100 Subject: [PATCH 2/2] Add AntSeed wrapped agent commands --- .../src/cli/commands/wrapped-tools.test.ts | 247 ++++++++++++ apps/cli/src/cli/commands/wrapped-tools.ts | 378 ++++++++++++++++++ apps/cli/src/cli/index.ts | 2 + apps/website/src/integrations/integrations.ts | 99 +++-- 4 files changed, 691 insertions(+), 35 deletions(-) create mode 100644 apps/cli/src/cli/commands/wrapped-tools.test.ts create mode 100644 apps/cli/src/cli/commands/wrapped-tools.ts diff --git a/apps/cli/src/cli/commands/wrapped-tools.test.ts b/apps/cli/src/cli/commands/wrapped-tools.test.ts new file mode 100644 index 00000000..565a3353 --- /dev/null +++ b/apps/cli/src/cli/commands/wrapped-tools.test.ts @@ -0,0 +1,247 @@ +import assert from 'node:assert/strict' +import { spawnSync } from 'node:child_process' +import { chmod, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { dirname, join } from 'node:path' +import test from 'node:test' +import { fileURLToPath } from 'node:url' +import { + buildCodexConfigArgs, + buildOpenCodeConfigContent, + normalizeAntseedBaseUrl, + parseWrappedToolArgs, + resolveDefaultAntseedBaseUrl, +} from './wrapped-tools.js' + +const ROOT_PROXY_URL = 'http://localhost:8378' +const V1_PROXY_URL = `${ROOT_PROXY_URL}/v1` +const MODEL_ID = 'deepseek-v4-flash' +const CLI_INDEX = join(dirname(fileURLToPath(import.meta.url)), '..', 'index.js') +const RECORDED_ENV_KEYS = ['ANTHROPIC_BASE_URL', 'ANTHROPIC_API_KEY', 'ANTSEED_API_KEY', 'OPENCODE_CONFIG'] as const + +type OpenCodeConfig = { + provider: Record + }> + model: string +} + +type ChildCliInvocation = { + argv: string[] + env: Record<(typeof RECORDED_ENV_KEYS)[number], string | null> + opencodeConfig?: OpenCodeConfig +} + +type TempWorkspace = { + binDir: string + configPath: string + dataDir: string +} + +type RecordingChildCli = { + readInvocation: () => Promise +} + +async function withTempWorkspace(fn: (paths: TempWorkspace) => Promise): Promise { + const root = await mkdtemp(join(tmpdir(), 'antseed-wrapper-')) + try { + const binDir = join(root, 'bin') + const dataDir = join(root, 'data') + await mkdir(binDir) + await mkdir(dataDir) + return await fn({ binDir, dataDir, configPath: join(dataDir, 'config.json') }) + } finally { + await rm(root, { recursive: true, force: true }) + } +} + +async function createRecordingChildCli(workspace: TempWorkspace, executable: string): Promise { + const capturePath = join(workspace.dataDir, `${executable}-invocation.json`) + const envKeys = JSON.stringify(RECORDED_ENV_KEYS) + const script = `#!/usr/bin/env node +import { readFileSync, writeFileSync } from 'node:fs'; + +const envKeys = ${envKeys}; +const env = Object.fromEntries(envKeys.map((key) => [key, process.env[key] ?? null])); +const payload = { argv: process.argv.slice(2), env }; + +if (process.env.OPENCODE_CONFIG) { + payload.opencodeConfig = JSON.parse(readFileSync(process.env.OPENCODE_CONFIG, 'utf8')); +} + +writeFileSync(${JSON.stringify(capturePath)}, JSON.stringify(payload, null, 2)); +` + const executablePath = join(workspace.binDir, executable) + await writeFile(executablePath, script, 'utf8') + await chmod(executablePath, 0o755) + + return { + readInvocation: async () => JSON.parse(await readFile(capturePath, 'utf8')) as ChildCliInvocation, + } +} + +function runWrapper(workspace: TempWorkspace, args: string[]): ReturnType { + return spawnSync( + process.execPath, + [CLI_INDEX, '--data-dir', workspace.dataDir, '--config', workspace.configPath, ...args], + { + encoding: 'utf8', + env: { ...process.env, PATH: `${workspace.binDir}:${process.env.PATH ?? ''}` }, + }, + ) +} + +test('parseWrappedToolArgs', async (t) => { + await t.test('consumes AntSeed flags and forwards child args', () => { + const parsed = parseWrappedToolArgs([ + '--model', + 'gpt-oss-120b', + '--sandbox', + 'workspace-write', + '--antseed-base-url=localhost:8378', + ]) + + assert.equal(parsed.model, 'gpt-oss-120b') + assert.equal(parsed.antseedBaseUrl, 'localhost:8378') + assert.deepEqual(parsed.childArgs, ['--sandbox', 'workspace-write']) + }) + + await t.test('keeps -- escape hatch for child args', () => { + const parsed = parseWrappedToolArgs(['--model=foo', '--', '--model', 'child-model']) + + assert.equal(parsed.model, 'foo') + assert.deepEqual(parsed.childArgs, ['--model', 'child-model']) + }) + + await t.test('rejects unsupported AntSeed wrapper flags', () => { + assert.throws( + () => parseWrappedToolArgs(['--antseed-peer', 'peer-id']), + /--antseed-peer is not supported by this wrapper/, + ) + }) +}) + +test('normalizeAntseedBaseUrl', async (t) => { + await t.test('adds protocol and /v1 to a root URL', () => { + assert.deepEqual(normalizeAntseedBaseUrl('localhost:8377'), { + root: 'http://localhost:8377', + v1: 'http://localhost:8377/v1', + }) + }) + + await t.test('derives root from a /v1 URL', () => { + assert.deepEqual(normalizeAntseedBaseUrl('http://localhost:8377/v1'), { + root: 'http://localhost:8377', + v1: 'http://localhost:8377/v1', + }) + }) +}) + +test('resolveDefaultAntseedBaseUrl prefers active buyer state over config', async () => { + await withTempWorkspace(async ({ dataDir, configPath }) => { + await writeFile(configPath, JSON.stringify({ buyer: { proxyPort: 8390 } })) + assert.equal(await resolveDefaultAntseedBaseUrl(dataDir, configPath), 'http://localhost:8390') + + await writeFile(join(dataDir, 'buyer.state.json'), JSON.stringify({ port: 8378 })) + assert.equal(await resolveDefaultAntseedBaseUrl(dataDir, configPath), 'http://localhost:8378') + }) +}) + +test('buildCodexConfigArgs uses ephemeral config overrides on the real Codex home', () => { + const args = buildCodexConfigArgs(V1_PROXY_URL, MODEL_ID) + + assert.equal(args.includes('--profile'), false) + assert.equal(args.includes('CODEX_HOME'), false) + assert.ok(args.includes('model_providers.antseed.name="AntSeed"')) + assert.ok(args.includes(`model_providers.antseed.base_url="${V1_PROXY_URL}"`)) + assert.ok(args.includes('model_providers.antseed.wire_api="responses"')) + assert.ok(args.includes('model_providers.antseed.env_key="ANTSEED_API_KEY"')) + assert.ok(args.includes(`model="${MODEL_ID}"`)) + assert.ok(args.includes('model_provider="antseed"')) +}) + +test('buildOpenCodeConfigContent configures AntSeed provider and selected model', () => { + const model = 'gpt-oss-120b' + const parsed = JSON.parse(buildOpenCodeConfigContent(V1_PROXY_URL, model)) as OpenCodeConfig + + assert.ok(parsed.provider.antseed) + assert.equal(parsed.provider.antseed.options.baseURL, V1_PROXY_URL) + assert.equal(parsed.model, `antseed/${model}`) + assert.ok(parsed.provider.antseed.models[model]) +}) + +test('wrapped tool execution', async (t) => { + await t.test('codex receives AntSeed config overrides and forwarded child args', async () => { + await withTempWorkspace(async (workspace) => { + const codex = await createRecordingChildCli(workspace, 'codex') + + const result = runWrapper(workspace, [ + 'codex', + '--model', + 'minimax-m2.5', + '--antseed-base-url', + 'localhost:8125', + '--', + '--version', + ]) + const invocation = await codex.readInvocation() + + assert.equal(result.status, 0) + assert.equal(String(result.stderr), '') + assert.match(String(result.stdout), /AntSeed proxy: http:\/\/localhost:8125\/v1/) + assert.ok(invocation.argv.includes('model_provider="antseed"')) + assert.equal(invocation.argv.at(-1), '--version') + assert.equal(invocation.env.ANTSEED_API_KEY, 'antseed') + }) + }) + + await t.test('claude receives Anthropic proxy env and model args', async () => { + await withTempWorkspace(async (workspace) => { + const claude = await createRecordingChildCli(workspace, 'claude') + + const result = runWrapper(workspace, [ + 'claude', + '--model', + 'claude-sonnet', + '--antseed-base-url', + 'localhost:8123/v1', + '--print', + ]) + const invocation = await claude.readInvocation() + + assert.equal(result.status, 0) + assert.equal(String(result.stderr), '') + assert.match(String(result.stdout), /AntSeed proxy: http:\/\/localhost:8123/) + assert.deepEqual(invocation.argv, ['--model', 'claude-sonnet', '--print']) + assert.equal(invocation.env.ANTHROPIC_BASE_URL, 'http://localhost:8123') + assert.equal(invocation.env.ANTHROPIC_API_KEY, 'antseed') + }) + }) + + await t.test('opencode receives generated config and forwarded child args', async () => { + await withTempWorkspace(async (workspace) => { + const opencode = await createRecordingChildCli(workspace, 'opencode') + + const result = runWrapper(workspace, [ + 'opencode', + '--model', + 'gpt-oss-120b', + '--antseed-base-url', + 'http://localhost:8124/v1', + 'run', + ]) + const invocation = await opencode.readInvocation() + const antseedProvider = invocation.opencodeConfig?.provider.antseed + + assert.equal(result.status, 0) + assert.equal(String(result.stderr), '') + assert.match(String(result.stdout), /AntSeed proxy: http:\/\/localhost:8124\/v1/) + assert.deepEqual(invocation.argv, ['run']) + assert.equal(invocation.opencodeConfig?.model, 'antseed/gpt-oss-120b') + assert.ok(antseedProvider) + assert.equal(antseedProvider.options.baseURL, 'http://localhost:8124/v1') + assert.ok(antseedProvider.models['gpt-oss-120b']) + }) + }) +}) diff --git a/apps/cli/src/cli/commands/wrapped-tools.ts b/apps/cli/src/cli/commands/wrapped-tools.ts new file mode 100644 index 00000000..499c3b83 --- /dev/null +++ b/apps/cli/src/cli/commands/wrapped-tools.ts @@ -0,0 +1,378 @@ +import type { Command } from 'commander' +import chalk from 'chalk' +import { spawn } from 'node:child_process' +import { accessSync, constants } from 'node:fs' +import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { delimiter, join } from 'node:path' +import { getGlobalOptions } from './types.js' + +type ToolName = 'codex' | 'claude' | 'opencode' + +type ToolConfig = { + executable: string +} + +const DEFAULT_PROXY_ROOT_URL = 'http://localhost:8377' +const DEFAULT_RUNTIME_API_KEY = 'antseed' +const OPENCODE_CONFIG_PREFIX = 'antseed-opencode-' + +const TOOL_CONFIGS: Record = { + codex: { + executable: 'codex', + }, + claude: { + executable: 'claude', + }, + opencode: { + executable: 'opencode', + }, +} + +const NOOP_CLEANUP = async (): Promise => {} + +export type ParsedWrappedToolArgs = { + childArgs: string[] + model: string | null + antseedBaseUrl: string | null +} + +type NormalizedBaseUrl = { + root: string + v1: string +} + +type PreparedInvocation = { + args: string[] + env: NodeJS.ProcessEnv + cleanup: () => Promise +} + +export function parseWrappedToolArgs(rawArgs: string[]): ParsedWrappedToolArgs { + const childArgs: string[] = [] + let model: string | null = null + let antseedBaseUrl: string | null = null + + for (let i = 0; i < rawArgs.length; i += 1) { + const arg = rawArgs[i]! + if (arg === '--') { + childArgs.push(...rawArgs.slice(i + 1)) + break + } + + const modelFlag = readValueFlag(rawArgs, i, '--model') + if (modelFlag) { + model = modelFlag.value + i = modelFlag.nextIndex + continue + } + + const baseUrlFlag = readValueFlag(rawArgs, i, '--antseed-base-url') + if (baseUrlFlag) { + antseedBaseUrl = baseUrlFlag.value + i = baseUrlFlag.nextIndex + continue + } + + if (arg.startsWith('--antseed-')) { + throw new Error(`${arg.split('=')[0]} is not supported by this wrapper`) + } + + childArgs.push(arg) + } + + return { + childArgs, + model: normalizeOptionalString(model), + antseedBaseUrl: normalizeOptionalString(antseedBaseUrl), + } +} + +function readValueFlag(args: string[], index: number, flag: string): { value: string; nextIndex: number } | null { + const arg = args[index]! + const inlineValue = readInlineValue(arg, flag) + if (inlineValue !== null) { + return { value: inlineValue, nextIndex: index } + } + if (arg !== flag) { + return null + } + + const value = args[index + 1] + if (!value || value.startsWith('-')) { + throw new Error(`${flag} requires a value`) + } + return { value, nextIndex: index + 1 } +} + +function readInlineValue(arg: string, flag: string): string | null { + const prefix = `${flag}=` + return arg.startsWith(prefix) ? arg.slice(prefix.length) : null +} + +export function normalizeAntseedBaseUrl(inputUrl: string | null | undefined): NormalizedBaseUrl { + const raw = normalizeOptionalString(inputUrl) ?? DEFAULT_PROXY_ROOT_URL + const withProtocol = /^[a-z][a-z0-9+.-]*:\/\//i.test(raw) ? raw : `http://${raw}` + const parsed = new URL(withProtocol) + parsed.hash = '' + parsed.search = '' + parsed.pathname = parsed.pathname.replace(/\/+$/, '') + + const path = parsed.pathname === '/' ? '' : parsed.pathname + if (path.toLowerCase() === '/v1') { + parsed.pathname = '' + const root = parsed.toString().replace(/\/$/, '') + return { root, v1: `${root}/v1` } + } + + const root = parsed.toString().replace(/\/$/, '') + return { root, v1: `${root}/v1` } +} + +export async function resolveDefaultAntseedBaseUrl(dataDir: string, configPath: string): Promise { + const statePort = await readJsonPort(join(dataDir, 'buyer.state.json'), ['port']) + if (statePort !== null) { + return `http://localhost:${statePort}` + } + + const configuredPort = await readJsonPort(configPath, ['buyer', 'proxyPort']) + if (configuredPort !== null) { + return `http://localhost:${configuredPort}` + } + + return DEFAULT_PROXY_ROOT_URL +} + +export function buildCodexConfigArgs(baseUrlV1: string, model: string): string[] { + return [ + '-c', + `model_providers.antseed.name=${tomlString('AntSeed')}`, + '-c', + `model_providers.antseed.base_url=${tomlString(baseUrlV1)}`, + '-c', + 'model_providers.antseed.wire_api="responses"', + '-c', + 'model_providers.antseed.env_key="ANTSEED_API_KEY"', + '-c', + `model_providers.antseed.env_key_instructions=${tomlString('The AntSeed wrapper sets ANTSEED_API_KEY automatically.')}`, + '-c', + `model=${tomlString(model)}`, + '-c', + 'model_provider="antseed"', + ] +} + +export function buildOpenCodeConfigContent(baseUrlV1: string, model: string): string { + return JSON.stringify({ + provider: { + antseed: { + npm: '@ai-sdk/openai-compatible', + name: 'AntSeed', + options: { + baseURL: baseUrlV1, + apiKey: 'antseed', + }, + models: { + [model]: { + name: `${model} (via AntSeed)`, + }, + }, + }, + }, + model: `antseed/${model}`, + }) +} + +function tomlString(value: string): string { + return JSON.stringify(value) +} + +function normalizeOptionalString(value: string | null | undefined): string | null { + if (value === null || value === undefined) return null + const trimmed = value.trim() + return trimmed.length > 0 ? trimmed : null +} + +async function readJsonPort(filePath: string, path: string[]): Promise { + try { + const parsed = JSON.parse(await readFile(filePath, 'utf-8')) as unknown + let current: unknown = parsed + for (const key of path) { + if (typeof current !== 'object' || current === null || !(key in current)) { + return null + } + current = (current as Record)[key] + } + const port = Number(current) + return Number.isInteger(port) && port > 0 && port <= 65535 ? port : null + } catch { + return null + } +} + +function resolveModel(model: string | null, toolName: ToolName): string { + const resolved = normalizeOptionalString(model) ?? normalizeOptionalString(process.env['ANTSEED_MODEL']) + if (resolved) return resolved + throw new Error(`${toolName} requires --model or ANTSEED_MODEL`) +} + +function resolveExecutable(command: string): string | null { + if (command.includes('/') || command.includes('\\')) { + return command + } + const pathEnv = process.env['PATH'] ?? '' + const extensions = process.platform === 'win32' + ? (process.env['PATHEXT'] ?? '.EXE;.CMD;.BAT;.COM').split(';') + : [''] + for (const dir of pathEnv.split(delimiter)) { + if (!dir) continue + for (const ext of extensions) { + const candidate = join(dir, `${command}${ext}`) + try { + accessSync(candidate, constants.X_OK) + return candidate + } catch { + // try next candidate + } + } + } + return null +} + +async function spawnInteractive(command: string, args: string[], env: NodeJS.ProcessEnv): Promise { + return await new Promise((resolveExit, reject) => { + const child = spawn(command, args, { + stdio: 'inherit', + env, + }) + child.once('error', reject) + child.once('exit', (code, signal) => { + if (typeof code === 'number') { + resolveExit(code) + } else { + console.log(chalk.yellow(`Child process exited from signal ${signal ?? 'unknown'}.`)) + resolveExit(1) + } + }) + }) +} + +async function prepareToolInvocation( + toolName: ToolName, + parsed: ParsedWrappedToolArgs, + baseUrl: NormalizedBaseUrl, +): Promise { + switch (toolName) { + case 'codex': + return prepareCodexInvocation(parsed, baseUrl) + case 'claude': + return prepareClaudeInvocation(parsed, baseUrl) + case 'opencode': + return await prepareOpenCodeInvocation(parsed, baseUrl) + } +} + +function prepareCodexInvocation(parsed: ParsedWrappedToolArgs, baseUrl: NormalizedBaseUrl): PreparedInvocation { + const model = resolveModel(parsed.model, 'codex') + return { + args: [...buildCodexConfigArgs(baseUrl.v1, model), ...parsed.childArgs], + env: withDefaultEnv('ANTSEED_API_KEY', DEFAULT_RUNTIME_API_KEY), + cleanup: NOOP_CLEANUP, + } +} + +function prepareClaudeInvocation(parsed: ParsedWrappedToolArgs, baseUrl: NormalizedBaseUrl): PreparedInvocation { + return { + args: parsed.model ? ['--model', parsed.model, ...parsed.childArgs] : parsed.childArgs, + env: { + ...withDefaultEnv('ANTHROPIC_API_KEY', DEFAULT_RUNTIME_API_KEY), + ANTHROPIC_BASE_URL: baseUrl.root, + }, + cleanup: NOOP_CLEANUP, + } +} + +async function prepareOpenCodeInvocation(parsed: ParsedWrappedToolArgs, baseUrl: NormalizedBaseUrl): Promise { + const model = resolveModel(parsed.model, 'opencode') + const configDir = await mkdtemp(join(tmpdir(), OPENCODE_CONFIG_PREFIX)) + const configPath = join(configDir, 'opencode.json') + await writeFile(configPath, buildOpenCodeConfigContent(baseUrl.v1, model), 'utf-8') + return { + args: parsed.childArgs, + env: { + ...process.env, + OPENCODE_CONFIG: configPath, + }, + cleanup: () => rm(configDir, { recursive: true, force: true }), + } +} + +function withDefaultEnv(name: string, fallback: string): NodeJS.ProcessEnv { + return { + ...process.env, + [name]: process.env[name] || fallback, + } +} + +function proxyDisplayUrl(toolName: ToolName, baseUrl: NormalizedBaseUrl): string { + return toolName === 'claude' ? baseUrl.root : baseUrl.v1 +} + +async function runWrappedTool(toolName: ToolName, rawArgs: string[], dataDir: string, configPath: string): Promise { + const tool = TOOL_CONFIGS[toolName] + let parsed: ParsedWrappedToolArgs + let baseUrl: NormalizedBaseUrl + let prepared: PreparedInvocation + try { + parsed = parseWrappedToolArgs(rawArgs) + baseUrl = normalizeAntseedBaseUrl(parsed.antseedBaseUrl ?? await resolveDefaultAntseedBaseUrl(dataDir, configPath)) + prepared = await prepareToolInvocation(toolName, parsed, baseUrl) + } catch (err) { + console.error(chalk.red((err as Error).message)) + process.exitCode = 1 + return + } + + const executable = resolveExecutable(tool.executable) + if (!executable) { + console.error(chalk.red(`"${tool.executable}" is not installed or is not on PATH.`)) + process.exit(1) + } + + console.log(chalk.dim(`AntSeed proxy: ${proxyDisplayUrl(toolName, baseUrl)}`)) + const exitCode = await spawnInteractive(executable, prepared.args, prepared.env).catch((err) => { + console.error(chalk.red(`Failed to run ${toolName}: ${(err as Error).message}`)) + return 1 + }).finally(async () => { + await prepared.cleanup().catch(() => {}) + }) + process.exitCode = exitCode +} + +function registerToolCommand(program: Command, toolName: ToolName): void { + const tool = TOOL_CONFIGS[toolName] + program + .command(toolName) + .description(`Run ${tool.executable} with AntSeed proxy settings`) + .allowUnknownOption(true) + .allowExcessArguments(true) + .argument('[args...]', `${tool.executable} arguments; AntSeed consumes --model and --antseed-base-url`) + .addHelpText('after', ` + +AntSeed wrapper flags: + --model Service/model id for the tool + --antseed-base-url Proxy URL (default: active buyer proxy/config) + +Unknown flags are forwarded to ${tool.executable}. +`) + .action(async (args: string[]) => { + const globalOpts = getGlobalOptions(program) + await runWrappedTool(toolName, args, globalOpts.dataDir, globalOpts.config) + }) +} + +export function registerWrappedToolCommands(program: Command): void { + registerToolCommand(program, 'codex') + registerToolCommand(program, 'claude') + registerToolCommand(program, 'opencode') +} diff --git a/apps/cli/src/cli/index.ts b/apps/cli/src/cli/index.ts index ff450d12..ae46c568 100755 --- a/apps/cli/src/cli/index.ts +++ b/apps/cli/src/cli/index.ts @@ -11,6 +11,7 @@ import { registerAgentCommand } from './commands/agent.js'; import { registerDevCommand } from './commands/dev.js'; import { registerPaymentsCommand } from './commands/payments.js'; import { registerMetricsCommand } from './commands/metrics.js'; +import { registerWrappedToolCommands } from './commands/wrapped-tools.js'; loadEnvFromFiles(); @@ -36,5 +37,6 @@ registerDevCommand(program); registerAgentCommand(program); registerPaymentsCommand(program); registerMetricsCommand(program); +registerWrappedToolCommands(program); program.parse(process.argv); diff --git a/apps/website/src/integrations/integrations.ts b/apps/website/src/integrations/integrations.ts index b70d0dca..73d27f37 100644 --- a/apps/website/src/integrations/integrations.ts +++ b/apps/website/src/integrations/integrations.ts @@ -146,9 +146,10 @@ export const integrations: Integration[] = [ format: 'anthropic-messages', setupMinutes: 2, status: 'verified', - oneLiner: "Anthropic's official CLI agent — drop-in via ANTHROPIC_BASE_URL.", + oneLiner: "Anthropic's official CLI agent — launch through AntSeed with `antseed claude`.", description: [ - 'Claude Code is the official CLI coding agent from Anthropic. It speaks the Anthropic Messages API natively, so it slots into AntSeed by simply pointing `ANTHROPIC_BASE_URL` at your local proxy.', + 'Claude Code is the official CLI coding agent from Anthropic. It speaks the Anthropic Messages API natively, so it slots into AntSeed through the `antseed claude` wrapper or by pointing `ANTHROPIC_BASE_URL` at your local proxy.', + '`antseed claude` resolves the active buyer proxy, sets the placeholder Anthropic API key for the child process, and forwards the rest of your Claude Code flags unchanged. Manual environment variables still work if you want to run `claude` directly.', 'No real Anthropic API key is needed — the AntSeed proxy authenticates each request with your local identity (`ANTSEED_IDENTITY_HEX`) and settles payments on-chain. The `ANTHROPIC_API_KEY` value is required by the Anthropic SDK only as a non-empty placeholder.', 'When Claude Code calls the Messages API, the proxy forwards the request to the peer you pinned in step 3 of the setup above. Whichever service ids that peer advertises (visible in antseed network peer <peerId>) become the valid --model values.', ], @@ -161,17 +162,25 @@ export const integrations: Integration[] = [ }, ], configure: [ + { + kind: 'code', + language: 'bash', + snippet: 'antseed claude --model claude-sonnet-4-6', + note: + 'Recommended: the wrapper reads the active buyer proxy from `buyer.state.json` or config, sets `ANTHROPIC_BASE_URL` and `ANTHROPIC_API_KEY` for Claude Code, and forwards extra Claude args. Add `--antseed-base-url http://host:port` only when your proxy is somewhere else.', + }, { kind: 'env', vars: { ANTHROPIC_BASE_URL: `http://localhost:${ANT_PORT}`, ANTHROPIC_API_KEY: 'antseed', }, + note: 'Manual equivalent if you want to run `claude` directly instead of through `antseed claude`.', }, ], modelHints: { suggested: ['claude-sonnet-4-6', 'claude-opus-4-7', 'deepseek-v4-flash'], - note: "Claude Code's `--model` flag passes the value to the Messages API unchanged. The valid set is whatever your pinned peer advertises — see the discovery commands below.", + note: "`antseed claude --model ` passes the value to Claude Code unchanged. The valid set is whatever your pinned peer advertises — see the discovery commands below.", }, test: [ { @@ -186,14 +195,15 @@ export const integrations: Integration[] = [ 'These are the only ids that work with `--model`. To switch peers, run `antseed network browse`, then `antseed buyer connection set --peer ` and re-check this list.', }, { - label: 'Start a Claude Code session against AntSeed', - command: 'claude --model claude-sonnet-4-6', + label: 'Start a Claude Code session through the wrapper', + command: 'antseed claude --model claude-sonnet-4-6', + note: 'Manual equivalent after exporting the env vars above: `claude --model claude-sonnet-4-6`.', }, ], troubleshooting: [ { problem: '"invalid x-api-key" or 401 from Anthropic SDK', - fix: 'The SDK requires *some* value for `ANTHROPIC_API_KEY`. Set it to any non-empty string (e.g. `antseed`). The proxy ignores the value.', + fix: '`antseed claude` sets `ANTHROPIC_API_KEY=antseed` for you. If you run `claude` directly, set the variable to any non-empty string; the proxy ignores the value.', }, { problem: 'Hangs forever on first message', @@ -213,7 +223,7 @@ export const integrations: Integration[] = [ { label: 'AntSeed skill: join-buyer', href: 'https://github.com/AntSeed/antseed/tree/main/skills/join-buyer' }, ], agentSummary: - 'Set ANTHROPIC_BASE_URL=http://localhost:8377 and ANTHROPIC_API_KEY=antseed, then `claude --model `. The valid service ids are returned by `curl http://localhost:8377/v1/models` after pinning a peer.', + 'Prefer `antseed claude --model `. It sets ANTHROPIC_BASE_URL and ANTHROPIC_API_KEY for Claude Code. Manual equivalent: set ANTHROPIC_BASE_URL=http://localhost:8377 and ANTHROPIC_API_KEY=antseed, then run `claude --model `.', }, { slug: 'codex', @@ -221,27 +231,36 @@ export const integrations: Integration[] = [ logo: 'openai.png', category: 'coding-agent', format: 'openai-chat', - setupMinutes: 3, + setupMinutes: 2, status: 'verified', - oneLiner: "OpenAI's official CLI coding agent — add an AntSeed profile to ~/.codex/config.toml.", + oneLiner: "OpenAI's official CLI coding agent — use `antseed codex` for per-run proxy config.", description: [ - "Codex is OpenAI's terminal coding agent. Recent versions ignore `OPENAI_BASE_URL` and instead read `~/.codex/config.toml`, where you declare custom inference providers under `[model_providers]` and bundle them into named `[profiles]` you can select with `--profile`.", - 'AntSeed plugs in as a `model_provider` pointed at the local buyer proxy. Pair it with a profile and you can swap between OpenAI proper and AntSeed by changing one flag.', + "Codex is OpenAI's terminal coding agent. Recent versions ignore `OPENAI_BASE_URL` and instead read provider config from Codex settings.", + '`antseed codex` supplies that provider config for one run with Codex `-c` overrides, points it at the active buyer proxy, sets the placeholder API key, and leaves your real `CODEX_HOME` untouched.', + 'If you prefer a persistent Codex setup, you can still add an AntSeed provider/profile to `~/.codex/config.toml`; the wrapper is the shortest path for one-off sessions.', ], install: [ { label: 'Install Codex globally', command: 'npm install -g @openai/codex' }, { label: 'Verify it runs', command: 'codex --version' }, ], configure: [ + { + kind: 'code', + language: 'bash', + snippet: 'antseed codex --model claude-sonnet-4-6', + note: + 'Recommended: the wrapper resolves the proxy URL, injects an AntSeed model provider with `wire_api = "responses"`, sets `ANTSEED_API_KEY=antseed`, and forwards extra Codex args. Put child flags after `--` when they look like wrapper flags.', + }, { kind: 'file', path: '~/.codex/config.toml', language: 'toml', - snippet: `# Register AntSeed as a custom model provider. + snippet: `# Manual alternative: register AntSeed as a custom model provider. [model_providers.antseed] name = "AntSeed" base_url = "http://localhost:${ANT_PORT}/v1" -wire_api = "chat" # or "responses" — AntSeed supports both +wire_api = "responses" +env_key = "ANTSEED_API_KEY" # Bundle the provider + a default model into a profile. [profiles.antseed] @@ -251,17 +270,17 @@ model_provider = "antseed" # Optional: make AntSeed the default profile so you don't need --profile every time. # profile = "antseed"`, note: - 'This must be your **user-level** `~/.codex/config.toml`. Codex silently ignores `model_provider` / `model_providers` if they appear in a project-local `./.codex/config.toml` and prints a one-line warning at launch (see Troubleshooting).', + 'Manual profile only: this must be your **user-level** `~/.codex/config.toml`. Also export `ANTSEED_API_KEY=antseed` before launching Codex directly. Project-local `./.codex/config.toml` provider blocks are ignored by Codex.', }, { kind: 'gui', instructions: - 'No API key is needed — the AntSeed proxy authenticates every request using your local identity, not an Authorization header. If Codex prompts for a key on first run, type any non-empty value and continue.', + 'No real OpenAI key is needed. The wrapper sets `ANTSEED_API_KEY` automatically; for manual profiles, export `ANTSEED_API_KEY=antseed` or enter any non-empty placeholder if Codex prompts.', }, ], modelHints: { suggested: ['claude-sonnet-4-6', 'deepseek-v3.1', 'kimi-k2.5', 'qwen-3-coder-480b'], - note: 'Set `model = ""` inside `[profiles.antseed]`, or override per-session with `codex --profile antseed --model `. Anything your pinned peer advertises works.', + note: 'Pass the peer service id to `antseed codex --model `. For a manual profile, set `model = ""` inside `[profiles.antseed]` or override with `codex --profile antseed --model `.', }, test: [ { @@ -276,9 +295,9 @@ model_provider = "antseed" 'Whatever appears here is a valid value for `model = ...` inside `[profiles.antseed]` (or for `codex --profile antseed --model `).', }, { - label: 'Run Codex against AntSeed', - command: 'codex --profile antseed', - note: 'Or pin a model for one session: `codex --profile antseed --model deepseek-v4-flash`.', + label: 'Run Codex through the wrapper', + command: 'antseed codex --model deepseek-v4-flash', + note: 'Manual profile equivalent: `ANTSEED_API_KEY=antseed codex --profile antseed --model deepseek-v4-flash`.', }, { label: 'Verify inference is actually paid through AntSeed', @@ -293,7 +312,7 @@ Deposits reserved: 0 USDC → 1 USDC`, troubleshooting: [ { problem: '`OPENAI_BASE_URL` / `OPENAI_API_KEY` are being ignored', - fix: 'Expected on Codex 0.40+ — it no longer reads OpenAI env vars and only loads providers from `~/.codex/config.toml`. Use the profile shown above and launch with `codex --profile antseed`.', + fix: 'Expected on recent Codex builds. Use `antseed codex --model ` so the wrapper injects the provider config for the current run, or use the manual `~/.codex/config.toml` profile above.', }, { problem: 'How can I tell if Codex is actually routing through AntSeed?', @@ -304,12 +323,12 @@ Deposits reserved: 0 USDC → 1 USDC`, fix: 'Provider settings must live in your **user-level** `~/.codex/config.toml`. Codex silently rejects them in a project-local `./.codex/config.toml` and falls back to its default (OpenAI). Move the `[model_providers.antseed]` and `[profiles.antseed]` blocks to `~/.codex/config.toml` and relaunch.', }, { - problem: 'Declaring the provider on the command line via `-c model_provider=…` / `-c model_providers.antseed=…`', - fix: 'Prefer `~/.codex/config.toml` + `--profile antseed`. Declaring the provider via `-c` flags has been observed to apply on `codex resume` but silently revert to OpenAI on a fresh `codex` launch. The config-file path is the only setup we reliably reproduce.', + problem: 'Hand-written Codex `-c` provider overrides behave inconsistently', + fix: 'Use `antseed codex --model ` so AntSeed supplies the complete provider block (`base_url`, `wire_api`, `env_key`, and `model_provider`) for the current run. If managing config yourself, keep the full provider/profile in user-level `~/.codex/config.toml`.', }, { - problem: 'Streaming stops after the first chunk', - fix: 'Switch `wire_api` between `"chat"` and `"responses"` in `[model_providers.antseed]`. AntSeed implements both; one may behave better with your Codex build.', + problem: 'Streaming stops after the first chunk with a manual profile', + fix: 'Use `antseed codex`, or set `wire_api = "responses"` in your manual `[model_providers.antseed]` block.', }, { problem: '`unknown profile: antseed`', @@ -325,7 +344,7 @@ Deposits reserved: 0 USDC → 1 USDC`, { label: 'Codex sample config', href: 'https://developers.openai.com/codex/config-sample' }, ], agentSummary: - 'Add [model_providers.antseed] (base_url=http://localhost:8377/v1, wire_api="chat") and [profiles.antseed] (model_provider="antseed", model="") to ~/.codex/config.toml, then run `codex --profile antseed`.', + 'Prefer `antseed codex --model `. It injects the AntSeed Codex provider for one run using base_url=http://localhost:8377/v1, wire_api="responses", and ANTSEED_API_KEY=antseed. Manual alternative: add [model_providers.antseed] and [profiles.antseed] to user-level ~/.codex/config.toml, then run `ANTSEED_API_KEY=antseed codex --profile antseed`.', }, { slug: 'opencode', @@ -333,12 +352,13 @@ Deposits reserved: 0 USDC → 1 USDC`, glyph: 'OC', category: 'coding-agent', format: 'openai-chat', - setupMinutes: 3, + setupMinutes: 2, status: 'verified', - oneLiner: 'Open-source AI coding agent — add AntSeed as a custom OpenAI-compatible provider.', + oneLiner: 'Open-source AI coding agent — launch through AntSeed with `antseed opencode`.', description: [ 'OpenCode is an MIT-licensed terminal coding agent built on the Vercel AI SDK. It supports 75+ providers out of the box and lets you register custom ones via opencode.json.', - 'AntSeed plugs in as a custom provider using the @ai-sdk/openai-compatible adapter — the same one OpenCode recommends for any OpenAI-compatible endpoint (LM Studio, llama.cpp, Atomic Chat, etc.). No environment variables, no ANTHROPIC_BASE_URL: the config lives in JSON.', + '`antseed opencode` creates that custom provider config in a temporary opencode.json, points OpenCode at it for the child process, and deletes it when the session exits. Manual project or global config still works if you want OpenCode to remember AntSeed outside the wrapper.', + 'AntSeed plugs in as a custom provider using the @ai-sdk/openai-compatible adapter — the same one OpenCode recommends for any OpenAI-compatible endpoint (LM Studio, llama.cpp, Atomic Chat, etc.). No ANTHROPIC_BASE_URL: OpenCode reads provider config from JSON.', 'Each model you want to use must be listed under models. The id has to match what the buyer proxy returns from GET /v1/models — i.e. a service id advertised by your currently-pinned peer.', ], install: [ @@ -349,6 +369,13 @@ Deposits reserved: 0 USDC → 1 USDC`, }, ], configure: [ + { + kind: 'code', + language: 'bash', + snippet: 'antseed opencode --model gpt-oss-120b', + note: + 'Recommended: the wrapper resolves the proxy URL, writes a temporary OpenCode config with one AntSeed model, sets `OPENCODE_CONFIG` for the child process, and forwards extra OpenCode args.', + }, { kind: 'file', path: 'opencode.json (project root, or ~/.config/opencode/opencode.json for global)', @@ -360,7 +387,8 @@ Deposits reserved: 0 USDC → 1 USDC`, "npm": "@ai-sdk/openai-compatible", "name": "AntSeed (peer-to-peer)", "options": { - "baseURL": "http://localhost:${ANT_PORT}/v1" + "baseURL": "http://localhost:${ANT_PORT}/v1", + "apiKey": "antseed" }, "models": { "claude-sonnet-4-6": { "name": "Claude Sonnet 4.6 (via AntSeed)" }, @@ -370,12 +398,13 @@ Deposits reserved: 0 USDC → 1 USDC`, } } }`, + note: 'Manual equivalent if you want OpenCode to keep AntSeed in its normal project or global config.', }, ], modelHints: { suggested: ['claude-sonnet-4-6', 'claude-opus-4-7', 'deepseek-v4-flash', 'gpt-oss-120b'], note: - 'The keys under `models` must exactly match service ids returned by `curl http://localhost:8377/v1/models`. If your pinned peer doesn\'t advertise an id, OpenCode will list it but every call to it returns `404 model_not_found`.', + '`antseed opencode --model ` generates a temporary config for that one id. In manual config, the keys under `models` must exactly match service ids returned by `curl http://localhost:8377/v1/models`.', }, test: [ { @@ -389,16 +418,16 @@ Deposits reserved: 0 USDC → 1 USDC`, note: 'Add or remove entries under `models` in `opencode.json` so they match this list.', }, { - label: 'Launch OpenCode in your project', - command: 'opencode', + label: 'Launch OpenCode through the wrapper', + command: 'antseed opencode --model gpt-oss-120b', note: - 'Inside the TUI, run `/models` and pick one of the AntSeed entries. OpenCode remembers your last selection per project.', + 'Extra OpenCode args are forwarded, so `antseed opencode --model gpt-oss-120b run` works too. Manual config equivalent: run `opencode`, then pick one of the AntSeed entries from `/models`.', }, ], troubleshooting: [ { problem: 'AntSeed doesn\'t appear in `/connect` or `/models`', - fix: 'OpenCode only loads providers declared in `opencode.json`. Make sure the file is in your project root (or `~/.config/opencode/opencode.json`) and that the JSON is valid — a stray comma silently disables the whole provider.', + fix: 'With `antseed opencode`, pass the service id via `--model`; the wrapper supplies a temporary config. With manual config, make sure `opencode.json` is in your project root (or `~/.config/opencode/opencode.json`) and that the JSON is valid — a stray comma silently disables the whole provider.', }, { problem: 'Model is listed but every call returns `model_not_found`', @@ -414,7 +443,7 @@ Deposits reserved: 0 USDC → 1 USDC`, { label: 'OpenCode repo', href: 'https://github.com/sst/opencode' }, ], agentSummary: - 'In opencode.json, register a custom provider with npm="@ai-sdk/openai-compatible", baseURL="http://localhost:8377/v1", and a `models` map whose keys match service ids from GET /v1/models. Then run `opencode` and pick the model via /models.', + 'Prefer `antseed opencode --model `. It creates a temporary OpenCode provider config using npm="@ai-sdk/openai-compatible", baseURL="http://localhost:8377/v1", apiKey="antseed", and one model entry. Manual alternative: put the same provider in opencode.json and run `opencode`.', }, { slug: 'pi',