diff --git a/integration-tests/simple-mcp-server.test.ts b/integration-tests/simple-mcp-server.test.ts index d8b6268d..d58bd982 100644 --- a/integration-tests/simple-mcp-server.test.ts +++ b/integration-tests/simple-mcp-server.test.ts @@ -213,7 +213,7 @@ describe('simple-mcp-server', () => { it('should add two numbers', async () => { // Test directory is already set up in before hook // Just run the command - MCP server config is in settings.json - const output = await rig.run('add 5 and 10'); + const output = await rig.run('add 5 and 10, use tool if you can.'); const foundToolCall = await rig.waitForToolCall('add'); diff --git a/packages/cli/src/nonInteractive/control/controllers/systemController.ts b/packages/cli/src/nonInteractive/control/controllers/systemController.ts index 7981a67b..c94187e7 100644 --- a/packages/cli/src/nonInteractive/control/controllers/systemController.ts +++ b/packages/cli/src/nonInteractive/control/controllers/systemController.ts @@ -19,6 +19,8 @@ import type { CLIControlInitializeRequest, CLIControlSetModelRequest, } from '../../types.js'; +import { CommandService } from '../../../services/CommandService.js'; +import { BuiltinCommandLoader } from '../../../services/BuiltinCommandLoader.js'; export class SystemController extends BaseController { /** @@ -141,31 +143,10 @@ export class SystemController extends BaseController { can_set_permission_mode: typeof this.context.config.setApprovalMode === 'function', can_set_model: typeof this.context.config.setModel === 'function', + /* TODO: sdkMcpServers support */ + can_handle_mcp_message: false, }; - // Check if MCP message handling is available - try { - const mcpProvider = this.context.config as unknown as { - getMcpServers?: () => Record | undefined; - }; - if (typeof mcpProvider.getMcpServers === 'function') { - const servers = mcpProvider.getMcpServers(); - capabilities['can_handle_mcp_message'] = Boolean( - servers && Object.keys(servers).length > 0, - ); - } else { - capabilities['can_handle_mcp_message'] = false; - } - } catch (error) { - if (this.context.debugMode) { - console.error( - '[SystemController] Failed to determine MCP capability:', - error, - ); - } - capabilities['can_handle_mcp_message'] = false; - } - return capabilities; } @@ -240,27 +221,45 @@ export class SystemController extends BaseController { /** * Handle supported_commands request * - * Returns list of supported control commands - * - * Note: This list should match the ControlRequestType enum in - * packages/sdk/typescript/src/types/controlRequests.ts + * Returns list of supported slash commands loaded dynamically */ private async handleSupportedCommands(): Promise> { - const commands = [ - 'initialize', - 'interrupt', - 'set_model', - 'supported_commands', - 'can_use_tool', - 'set_permission_mode', - 'mcp_message', - 'mcp_server_status', - 'hook_callback', - ]; + const slashCommands = await this.loadSlashCommandNames(); return { subtype: 'supported_commands', - commands, + commands: slashCommands, }; } + + /** + * Load slash command names using CommandService + * + * @returns Promise resolving to array of slash command names + */ + private async loadSlashCommandNames(): Promise { + const controller = new AbortController(); + try { + const service = await CommandService.create( + [new BuiltinCommandLoader(this.context.config)], + controller.signal, + ); + const names = new Set(); + const commands = service.getCommands(); + for (const command of commands) { + names.add(command.name); + } + return Array.from(names).sort(); + } catch (error) { + if (this.context.debugMode) { + console.error( + '[SystemController] Failed to load slash commands:', + error, + ); + } + return []; + } finally { + controller.abort(); + } + } } diff --git a/packages/cli/src/nonInteractive/io/BaseJsonOutputAdapter.ts b/packages/cli/src/nonInteractive/io/BaseJsonOutputAdapter.ts index 551ea9ff..915fb721 100644 --- a/packages/cli/src/nonInteractive/io/BaseJsonOutputAdapter.ts +++ b/packages/cli/src/nonInteractive/io/BaseJsonOutputAdapter.ts @@ -13,7 +13,11 @@ import type { ServerGeminiStreamEvent, TaskResultDisplay, } from '@qwen-code/qwen-code-core'; -import { GeminiEventType, ToolErrorType } from '@qwen-code/qwen-code-core'; +import { + GeminiEventType, + ToolErrorType, + parseAndFormatApiError, +} from '@qwen-code/qwen-code-core'; import type { Part, GenerateContentResponseUsageMetadata } from '@google/genai'; import type { CLIAssistantMessage, @@ -600,6 +604,18 @@ export abstract class BaseJsonOutputAdapter { } this.finalizePendingBlocks(state, null); break; + case GeminiEventType.Error: { + // Format the error message using parseAndFormatApiError for consistency + // with interactive mode error display + const errorText = parseAndFormatApiError( + event.value.error, + this.config.getContentGeneratorConfig()?.authType, + undefined, + this.config.getModel(), + ); + this.appendText(state, errorText, null); + break; + } default: break; } diff --git a/packages/cli/src/nonInteractive/session.test.ts b/packages/cli/src/nonInteractive/session.test.ts index 61643fb3..6670d4c2 100644 --- a/packages/cli/src/nonInteractive/session.test.ts +++ b/packages/cli/src/nonInteractive/session.test.ts @@ -69,6 +69,7 @@ function createConfig(overrides: ConfigOverrides = {}): Config { getDebugMode: () => false, getApprovalMode: () => 'auto', getOutputFormat: () => 'stream-json', + initialize: vi.fn(), }; return { ...base, ...overrides } as unknown as Config; } diff --git a/packages/cli/src/nonInteractiveCli.ts b/packages/cli/src/nonInteractiveCli.ts index 77e4f980..6f96d62b 100644 --- a/packages/cli/src/nonInteractiveCli.ts +++ b/packages/cli/src/nonInteractiveCli.ts @@ -17,6 +17,7 @@ import { OutputFormat, InputFormat, uiTelemetryService, + parseAndFormatApiError, } from '@qwen-code/qwen-code-core'; import type { Content, Part, PartListUnion } from '@google/genai'; import type { CLIUserMessage, PermissionMode } from './nonInteractive/types.js'; @@ -210,6 +211,15 @@ export async function runNonInteractive( process.stdout.write(event.value); } else if (event.type === GeminiEventType.ToolCallRequest) { toolCallRequests.push(event.value); + } else if (event.type === GeminiEventType.Error) { + // Format and output the error message for text mode + const errorText = parseAndFormatApiError( + event.value.error, + config.getContentGeneratorConfig()?.authType, + undefined, + config.getModel(), + ); + process.stderr.write(`${errorText}\n`); } } } diff --git a/packages/sdk-typescript/scripts/build.js b/packages/sdk-typescript/scripts/build.js index e78f161a..db0632cf 100755 --- a/packages/sdk-typescript/scripts/build.js +++ b/packages/sdk-typescript/scripts/build.js @@ -24,7 +24,7 @@ execSync('tsc --project tsconfig.build.json', { try { execSync( - 'npx dts-bundle-generator --project tsconfig.build.json -o dist/index.d.ts src/index.ts --no-check', + 'npx dts-bundle-generator --project tsconfig.build.json -o dist/index.d.ts src/index.ts', { stdio: 'inherit', cwd: rootDir, diff --git a/packages/sdk-typescript/vitest.config.ts b/packages/sdk-typescript/vitest.config.ts index aef50ffd..f46dc537 100644 --- a/packages/sdk-typescript/vitest.config.ts +++ b/packages/sdk-typescript/vitest.config.ts @@ -38,7 +38,6 @@ export default defineConfig({ }, testTimeout: testTimeoutMs, hookTimeout: 10000, - globalSetup: './test/e2e/globalSetup.ts', }, resolve: { alias: {