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
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"scripts": {
"dev": "bun --watch src/blade.tsx",
"dev:serve": "bun --watch src/blade.tsx serve --port 4097",
"build": "rm -rf dist && bun run scripts/build.ts",
"build": "rm -rf dist && node scripts/build-cli.js",
"start": "bun run dist/blade.js",
"test": "node scripts/test.js",
"test:all": "vitest run --config vitest.config.ts",
Expand Down
33 changes: 33 additions & 0 deletions packages/cli/scripts/build-cli.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { execSync } from 'node:child_process';
import { mkdirSync, writeFileSync } from 'node:fs';
import path from 'node:path';

const rootDir = process.cwd();

function runTsc() {
console.log('Building CLI with TypeScript (npx tsc)...');
execSync('npx tsc -p tsconfig.json', {
stdio: 'inherit',
});
}

function createEntry() {
const distDir = path.join(rootDir, 'dist');
mkdirSync(distDir, { recursive: true });

const entry = `#!/usr/bin/env node
import('./src/blade.js')
.then((m) => (typeof m.main === 'function' ? m.main() : undefined))
.catch((err) => {
console.error(err);
process.exit(1);
});
`;

const entryPath = path.join(distDir, 'blade.js');
writeFileSync(entryPath, entry, { mode: 0o755 });
console.log('✓ CLI entry created at dist/blade.js');
}

runTsc();
createEntry();
24 changes: 22 additions & 2 deletions packages/cli/src/agent/Agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,23 @@ export class Agent {
}

private resolveModelConfig(requestedModelId?: string): ModelConfig {
const modelId = requestedModelId && requestedModelId !== 'inherit' ? requestedModelId : undefined;
// 🆕 1. 优先检查运行时选项中是否直接提供了模型配置
const { provider, model, apiKey, baseUrl } = this.runtimeOptions;
if (provider && model && apiKey) {
return {
id: 'runtime-config',
name: model,
provider,
model,
apiKey,
baseUrl: baseUrl ?? '',
} as ModelConfig;
}

const modelId =
requestedModelId && requestedModelId !== 'inherit'
? requestedModelId
: undefined;
const modelConfig = modelId ? getModelById(modelId) : getCurrentModel();
if (!modelConfig) {
throw new Error(`❌ 模型配置未找到: ${modelId ?? 'current'}`);
Expand Down Expand Up @@ -208,8 +224,12 @@ export class Agent {
await ensureStoreInitialized();

// 1. 检查是否有可用的模型配置
// 如果运行时直接提供了模型配置,则无需检查 Store 中的模型
const models = getAllModels();
if (models.length === 0) {
const hasRuntimeConfig =
options.provider && options.model && options.apiKey;

if (!hasRuntimeConfig && models.length === 0) {
throw new Error(
'❌ 没有可用的模型配置\n\n' +
'请先使用以下命令添加模型:\n' +
Expand Down
6 changes: 6 additions & 0 deletions packages/cli/src/agent/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@ export interface AgentOptions {
toolWhitelist?: string[]; // 工具白名单(仅允许指定工具)
modelId?: string;

// 临时模型配置(用于 CLI 参数直接传入,绕过 Store)
provider?: string;
model?: string;
apiKey?: string;
baseUrl?: string;

// MCP 配置
mcpConfig?: string[]; // CLI 参数:MCP 配置文件路径或 JSON 字符串数组
strictMcpConfig?: boolean; // CLI 参数:严格模式,仅使用 --mcp-config 指定的配置
Expand Down
17 changes: 15 additions & 2 deletions packages/cli/src/blade.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,14 @@ import { installCommands } from './commands/install.js';
import { mcpCommands } from './commands/mcp.js';
import { handlePrintMode } from './commands/print.js';
import { serveCommand } from './commands/serve.js';
import { statsCommand } from './commands/stats.js';
import { updateCommands } from './commands/update.js';
import { webCommand } from './commands/web.js';
import { Logger } from './logging/Logger.js';
import { initializeGracefulShutdown } from './services/GracefulShutdown.js';
import { checkVersionOnStartup } from './services/VersionChecker.js';
import type { AppProps } from './ui/App.js';
import { AppWrapper as BladeApp } from './ui/App.js';
import { runHeadlessChat } from './commands/headless.js';

// ⚠️ 关键:在创建任何 logger 之前,先解析 --debug 参数并设置全局配置
// 这样可以确保所有 logger(包括 middleware、commands 中的)都能正确输出到终端
Expand Down Expand Up @@ -115,6 +116,7 @@ export async function main() {
.command(installCommands)
.command(webCommand)
.command(serveCommand)
.command(statsCommand)

// 自动生成补全(隐藏,避免干扰普通用户)
.completion('completion', false)
Expand Down Expand Up @@ -153,12 +155,20 @@ export async function main() {
// 不定义 positional,避免在 --help 中显示 Positionals 部分
},
async (argv) => {
// 启动 UI 模式
// 从 argv._ 中获取额外的参数作为 initialMessage
const nonOptionArgs = (argv._ as string[]).slice(1); // 跳过命令名
const initialMessage =
nonOptionArgs.length > 0 ? nonOptionArgs.join(' ') : undefined;

// Headless 模式:不渲染 Ink UI,直接通过 Agent 输出结果
if (argv.headless) {
await runHeadlessChat({
...(argv as Record<string, unknown>),
initialMessage,
});
return;
}

// 启动 React UI - 传递所有选项
const appProps = {
...argv,
Expand All @@ -175,6 +185,9 @@ export async function main() {
delete appProps.$0;
delete appProps.message;

// 延迟加载 UI 组件,避免在纯 CLI 场景下加载所有 UI 依赖
const { AppWrapper: BladeApp } = await import('./ui/App.js');

render(React.createElement(BladeApp, appProps), {
patchConsole: true,
exitOnCtrlC: false, // 由 useCtrlCHandler 处理(支持智能双击退出)
Expand Down
28 changes: 28 additions & 0 deletions packages/cli/src/cli/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ export const globalOptions = {
describe: 'Print response and exit (useful for pipes)',
group: 'Output Options:',
},
headless: {
type: 'boolean',
describe:
'Run in headless (non-TUI) mode, printing responses directly to stdout',
group: 'Output Options:',
},
'output-format': {
alias: ['outputFormat'],
type: 'string',
Expand Down Expand Up @@ -170,6 +176,28 @@ export const globalOptions = {
describe: 'Use a specific session ID for the conversation',
group: 'Session Options:',
},
provider: {
type: 'string',
describe: 'AI provider to use (e.g. openai, anthropic)',
group: 'AI Options:',
},
model: {
type: 'string',
describe: 'AI model to use (e.g. gpt-4o, claude-3-5-sonnet)',
group: 'AI Options:',
},
'api-key': {
alias: ['apiKey'],
type: 'string',
describe: 'API key for the AI provider',
group: 'AI Options:',
},
'base-url': {
alias: ['baseUrl'],
type: 'string',
describe: 'Base URL for the AI provider',
group: 'AI Options:',
},
// TODO: 未实现 - 需要解析 JSON 并配置自定义 Agent
agents: {
type: 'string',
Expand Down
5 changes: 5 additions & 0 deletions packages/cli/src/cli/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
export interface GlobalOptions {
debug?: string;
print?: boolean;
headless?: boolean;
outputFormat?: 'text' | 'json' | 'stream-json';
includePartialMessages?: boolean;
inputFormat?: 'text' | 'stream-json';
Expand All @@ -29,6 +30,10 @@ export interface GlobalOptions {
settingSources?: string;
maxTurns?: number;
pluginDir?: string[];
provider?: string;
model?: string;
apiKey?: string;
baseUrl?: string;
}

export interface DoctorOptions extends GlobalOptions {}
Expand Down
Loading
Loading