This commit is contained in:
nrslib 2026-01-31 11:22:58 +09:00
parent 6468fa6345
commit 4b924851a8
20 changed files with 462 additions and 41 deletions

View File

@ -21,6 +21,12 @@ provider: claude
# Codex: gpt-5.2-codex, gpt-5.1-codex, etc.
# model: sonnet
# Anthropic API key (optional, overridden by TAKT_ANTHROPIC_API_KEY env var)
# anthropic_api_key: ""
# OpenAI API key (optional, overridden by TAKT_OPENAI_API_KEY env var)
# openai_api_key: ""
# Debug settings (optional)
# debug:
# enabled: false

View File

@ -21,6 +21,12 @@ provider: claude
# Codex: gpt-5.2-codex, gpt-5.1-codex など
# model: sonnet
# Anthropic APIキー (オプション、環境変数 TAKT_ANTHROPIC_API_KEY で上書き可能)
# anthropic_api_key: ""
# OpenAI APIキー (オプション、環境変数 TAKT_OPENAI_API_KEY で上書き可能)
# openai_api_key: ""
# デバッグ設定 (オプション)
# debug:
# enabled: false

View File

@ -0,0 +1,289 @@
/**
* Tests for API key authentication feature
*
* Tests the resolution logic for Anthropic and OpenAI API keys:
* - Environment variable priority over config.yaml
* - Config.yaml fallback when env var is not set
* - Undefined when neither is set
* - Schema validation for API key fields
* - GlobalConfig load/save round-trip with API keys
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { mkdirSync, rmSync, writeFileSync, readFileSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { randomUUID } from 'node:crypto';
import { GlobalConfigSchema } from '../models/schemas.js';
// Mock paths module to redirect config to temp directory
const testId = randomUUID();
const testDir = join(tmpdir(), `takt-api-key-test-${testId}`);
const taktDir = join(testDir, '.takt');
const configPath = join(taktDir, 'config.yaml');
vi.mock('../config/paths.js', async (importOriginal) => {
const original = await importOriginal() as Record<string, unknown>;
return {
...original,
getGlobalConfigPath: () => configPath,
getTaktDir: () => taktDir,
};
});
// Import after mocking
const { loadGlobalConfig, saveGlobalConfig, resolveAnthropicApiKey, resolveOpenaiApiKey } = await import('../config/globalConfig.js');
describe('GlobalConfigSchema API key fields', () => {
it('should accept config without API keys', () => {
const result = GlobalConfigSchema.parse({
language: 'en',
});
expect(result.anthropic_api_key).toBeUndefined();
expect(result.openai_api_key).toBeUndefined();
});
it('should accept config with anthropic_api_key', () => {
const result = GlobalConfigSchema.parse({
language: 'en',
anthropic_api_key: 'sk-ant-test-key',
});
expect(result.anthropic_api_key).toBe('sk-ant-test-key');
});
it('should accept config with openai_api_key', () => {
const result = GlobalConfigSchema.parse({
language: 'en',
openai_api_key: 'sk-openai-test-key',
});
expect(result.openai_api_key).toBe('sk-openai-test-key');
});
it('should accept config with both API keys', () => {
const result = GlobalConfigSchema.parse({
language: 'en',
anthropic_api_key: 'sk-ant-key',
openai_api_key: 'sk-openai-key',
});
expect(result.anthropic_api_key).toBe('sk-ant-key');
expect(result.openai_api_key).toBe('sk-openai-key');
});
});
describe('GlobalConfig load/save with API keys', () => {
beforeEach(() => {
mkdirSync(taktDir, { recursive: true });
});
afterEach(() => {
rmSync(testDir, { recursive: true, force: true });
});
it('should load config with API keys from YAML', () => {
const yaml = [
'language: en',
'trusted_directories: []',
'default_workflow: default',
'log_level: info',
'provider: claude',
'anthropic_api_key: sk-ant-from-yaml',
'openai_api_key: sk-openai-from-yaml',
].join('\n');
writeFileSync(configPath, yaml, 'utf-8');
const config = loadGlobalConfig();
expect(config.anthropicApiKey).toBe('sk-ant-from-yaml');
expect(config.openaiApiKey).toBe('sk-openai-from-yaml');
});
it('should load config without API keys', () => {
const yaml = [
'language: en',
'trusted_directories: []',
'default_workflow: default',
'log_level: info',
'provider: claude',
].join('\n');
writeFileSync(configPath, yaml, 'utf-8');
const config = loadGlobalConfig();
expect(config.anthropicApiKey).toBeUndefined();
expect(config.openaiApiKey).toBeUndefined();
});
it('should save and reload config with API keys', () => {
// Write initial config
const yaml = [
'language: en',
'trusted_directories: []',
'default_workflow: default',
'log_level: info',
'provider: claude',
].join('\n');
writeFileSync(configPath, yaml, 'utf-8');
const config = loadGlobalConfig();
config.anthropicApiKey = 'sk-ant-saved';
config.openaiApiKey = 'sk-openai-saved';
saveGlobalConfig(config);
const reloaded = loadGlobalConfig();
expect(reloaded.anthropicApiKey).toBe('sk-ant-saved');
expect(reloaded.openaiApiKey).toBe('sk-openai-saved');
});
it('should not persist API keys when not set', () => {
const yaml = [
'language: en',
'trusted_directories: []',
'default_workflow: default',
'log_level: info',
'provider: claude',
].join('\n');
writeFileSync(configPath, yaml, 'utf-8');
const config = loadGlobalConfig();
saveGlobalConfig(config);
const content = readFileSync(configPath, 'utf-8');
expect(content).not.toContain('anthropic_api_key');
expect(content).not.toContain('openai_api_key');
});
});
describe('resolveAnthropicApiKey', () => {
const originalEnv = process.env['TAKT_ANTHROPIC_API_KEY'];
beforeEach(() => {
mkdirSync(taktDir, { recursive: true });
});
afterEach(() => {
if (originalEnv !== undefined) {
process.env['TAKT_ANTHROPIC_API_KEY'] = originalEnv;
} else {
delete process.env['TAKT_ANTHROPIC_API_KEY'];
}
rmSync(testDir, { recursive: true, force: true });
});
it('should return env var when set', () => {
process.env['TAKT_ANTHROPIC_API_KEY'] = 'sk-ant-from-env';
const yaml = [
'language: en',
'trusted_directories: []',
'default_workflow: default',
'log_level: info',
'provider: claude',
'anthropic_api_key: sk-ant-from-yaml',
].join('\n');
writeFileSync(configPath, yaml, 'utf-8');
const key = resolveAnthropicApiKey();
expect(key).toBe('sk-ant-from-env');
});
it('should fall back to config when env var is not set', () => {
delete process.env['TAKT_ANTHROPIC_API_KEY'];
const yaml = [
'language: en',
'trusted_directories: []',
'default_workflow: default',
'log_level: info',
'provider: claude',
'anthropic_api_key: sk-ant-from-yaml',
].join('\n');
writeFileSync(configPath, yaml, 'utf-8');
const key = resolveAnthropicApiKey();
expect(key).toBe('sk-ant-from-yaml');
});
it('should return undefined when neither env var nor config is set', () => {
delete process.env['TAKT_ANTHROPIC_API_KEY'];
const yaml = [
'language: en',
'trusted_directories: []',
'default_workflow: default',
'log_level: info',
'provider: claude',
].join('\n');
writeFileSync(configPath, yaml, 'utf-8');
const key = resolveAnthropicApiKey();
expect(key).toBeUndefined();
});
it('should return undefined when config file does not exist', () => {
delete process.env['TAKT_ANTHROPIC_API_KEY'];
// No config file created
rmSync(testDir, { recursive: true, force: true });
const key = resolveAnthropicApiKey();
expect(key).toBeUndefined();
});
});
describe('resolveOpenaiApiKey', () => {
const originalEnv = process.env['TAKT_OPENAI_API_KEY'];
beforeEach(() => {
mkdirSync(taktDir, { recursive: true });
});
afterEach(() => {
if (originalEnv !== undefined) {
process.env['TAKT_OPENAI_API_KEY'] = originalEnv;
} else {
delete process.env['TAKT_OPENAI_API_KEY'];
}
rmSync(testDir, { recursive: true, force: true });
});
it('should return env var when set', () => {
process.env['TAKT_OPENAI_API_KEY'] = 'sk-openai-from-env';
const yaml = [
'language: en',
'trusted_directories: []',
'default_workflow: default',
'log_level: info',
'provider: claude',
'openai_api_key: sk-openai-from-yaml',
].join('\n');
writeFileSync(configPath, yaml, 'utf-8');
const key = resolveOpenaiApiKey();
expect(key).toBe('sk-openai-from-env');
});
it('should fall back to config when env var is not set', () => {
delete process.env['TAKT_OPENAI_API_KEY'];
const yaml = [
'language: en',
'trusted_directories: []',
'default_workflow: default',
'log_level: info',
'provider: claude',
'openai_api_key: sk-openai-from-yaml',
].join('\n');
writeFileSync(configPath, yaml, 'utf-8');
const key = resolveOpenaiApiKey();
expect(key).toBe('sk-openai-from-yaml');
});
it('should return undefined when neither env var nor config is set', () => {
delete process.env['TAKT_OPENAI_API_KEY'];
const yaml = [
'language: en',
'trusted_directories: []',
'default_workflow: default',
'log_level: info',
'provider: claude',
].join('\n');
writeFileSync(configPath, yaml, 'utf-8');
const key = resolveOpenaiApiKey();
expect(key).toBeUndefined();
});
});

View File

@ -31,6 +31,8 @@ export interface ClaudeCallOptions {
onAskUserQuestion?: AskUserQuestionHandler;
/** Bypass all permission checks (sacrifice-my-pc mode) */
bypassPermissions?: boolean;
/** Anthropic API key to inject via env (bypasses CLI auth) */
anthropicApiKey?: string;
}
/**
@ -108,6 +110,7 @@ export async function callClaude(
onPermissionRequest: options.onPermissionRequest,
onAskUserQuestion: options.onAskUserQuestion,
bypassPermissions: options.bypassPermissions,
anthropicApiKey: options.anthropicApiKey,
};
const result = await executeClaudeCli(prompt, spawnOptions);
@ -146,6 +149,7 @@ export async function callClaudeCustom(
onPermissionRequest: options.onPermissionRequest,
onAskUserQuestion: options.onAskUserQuestion,
bypassPermissions: options.bypassPermissions,
anthropicApiKey: options.anthropicApiKey,
};
const result = await executeClaudeCli(prompt, spawnOptions);
@ -272,6 +276,7 @@ export async function callClaudeSkill(
onPermissionRequest: options.onPermissionRequest,
onAskUserQuestion: options.onAskUserQuestion,
bypassPermissions: options.bypassPermissions,
anthropicApiKey: options.anthropicApiKey,
};
const result = await executeClaudeCli(fullPrompt, spawnOptions);

View File

@ -25,6 +25,7 @@ import {
createCanUseToolCallback,
createAskUserQuestionHooks,
} from './options-builder.js';
import { resolveAnthropicApiKey } from '../config/globalConfig.js';
import type {
StreamCallback,
PermissionHandler,
@ -49,6 +50,8 @@ export interface ExecuteOptions {
onAskUserQuestion?: AskUserQuestionHandler;
/** Bypass all permission checks (sacrifice-my-pc mode) */
bypassPermissions?: boolean;
/** Anthropic API key to inject via env (bypasses CLI auth) */
anthropicApiKey?: string;
}
/**
@ -91,6 +94,14 @@ function buildSdkOptions(options: ExecuteOptions): Options {
if (canUseTool) sdkOptions.canUseTool = canUseTool;
if (hooks) sdkOptions.hooks = hooks;
const anthropicApiKey = options.anthropicApiKey ?? resolveAnthropicApiKey();
if (anthropicApiKey) {
sdkOptions.env = {
...process.env as Record<string, string>,
ANTHROPIC_API_KEY: anthropicApiKey,
};
}
if (options.onStream) {
sdkOptions.includePartialMessages = true;
}

View File

@ -69,6 +69,8 @@ export interface ClaudeSpawnOptions {
onAskUserQuestion?: AskUserQuestionHandler;
/** Bypass all permission checks (sacrifice-my-pc mode) */
bypassPermissions?: boolean;
/** Anthropic API key to inject via env (bypasses CLI auth) */
anthropicApiKey?: string;
}
/**

View File

@ -154,12 +154,16 @@ export function sdkMessageToStreamEvent(
case 'result': {
const resultMsg = message as SDKResultMessage;
const errors = resultMsg.subtype !== 'success' && resultMsg.errors?.length
? resultMsg.errors.join('\n')
: undefined;
callback({
type: 'result',
data: {
result: resultMsg.subtype === 'success' ? resultMsg.result : '',
sessionId: resultMsg.session_id,
success: resultMsg.subtype === 'success',
error: errors,
},
});
break;

View File

@ -44,6 +44,7 @@ export interface ResultEventData {
result: string;
sessionId: string;
success: boolean;
error?: string;
}
export interface ErrorEventData {

View File

@ -19,6 +19,8 @@ export interface CodexCallOptions {
systemPrompt?: string;
/** Enable streaming mode with callback (best-effort) */
onStream?: StreamCallback;
/** OpenAI API key (bypasses CLI auth) */
openaiApiKey?: string;
}
function extractThreadId(value: unknown): string | undefined {
@ -94,6 +96,7 @@ function emitResult(
result,
sessionId: sessionId || 'unknown',
success,
error: success ? undefined : result || undefined,
},
});
}
@ -341,7 +344,7 @@ export async function callCodex(
prompt: string,
options: CodexCallOptions
): Promise<AgentResponse> {
const codex = new Codex();
const codex = new Codex(options.openaiApiKey ? { apiKey: options.openaiApiKey } : undefined);
const threadOptions = {
model: options.model,
workingDirectory: options.cwd,

View File

@ -44,6 +44,7 @@ interface ConversationMessage {
interface CallAIResult {
content: string;
sessionId?: string;
success: boolean;
}
/**
@ -111,7 +112,8 @@ async function callAI(
});
display.flush();
return { content: response.content, sessionId: response.sessionId };
const success = response.status !== 'blocked';
return { content: response.content, sessionId: response.sessionId, success };
}
export interface InteractiveModeResult {
@ -138,7 +140,7 @@ export async function interactiveMode(cwd: string, initialInput?: string): Promi
const history: ConversationMessage[] = [];
const agentName = 'interactive';
const savedSessions = loadAgentSessions(cwd);
const savedSessions = loadAgentSessions(cwd, providerType);
let sessionId: string | undefined = savedSessions[agentName];
info('Interactive mode - describe your task. Commands: /go (execute), /cancel (exit)');
@ -147,26 +149,47 @@ export async function interactiveMode(cwd: string, initialInput?: string): Promi
}
console.log();
/** Call AI with automatic retry on session error (stale/invalid session ID). */
async function callAIWithRetry(prompt: string): Promise<CallAIResult | null> {
const display = new StreamDisplay('assistant');
try {
const result = await callAI(provider, prompt, cwd, model, sessionId, display);
// If session failed, clear it and retry without session
if (!result.success && sessionId) {
log.info('Session invalid, retrying without session');
sessionId = undefined;
const retryDisplay = new StreamDisplay('assistant');
const retry = await callAI(provider, prompt, cwd, model, undefined, retryDisplay);
if (retry.sessionId) {
sessionId = retry.sessionId;
updateAgentSession(cwd, agentName, sessionId, providerType);
}
return retry;
}
if (result.sessionId) {
sessionId = result.sessionId;
updateAgentSession(cwd, agentName, sessionId, providerType);
}
return result;
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
log.error('AI call failed', { error: msg });
console.log(chalk.red(`Error: ${msg}`));
console.log();
return null;
}
}
// Process initial input if provided (e.g. from `takt a`)
if (initialInput) {
history.push({ role: 'user', content: initialInput });
log.debug('Processing initial input', { initialInput, sessionId });
const display = new StreamDisplay('assistant');
try {
const result = await callAI(provider, initialInput, cwd, model, sessionId, display);
if (result.sessionId) {
sessionId = result.sessionId;
updateAgentSession(cwd, agentName, sessionId);
}
const result = await callAIWithRetry(initialInput);
if (result) {
history.push({ role: 'assistant', content: result.content });
console.log();
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
log.error('AI call failed for initial input', { error: msg });
console.log(chalk.red(`Error: ${msg}`));
console.log();
} else {
history.pop();
}
}
@ -211,21 +234,11 @@ export async function interactiveMode(cwd: string, initialInput?: string): Promi
log.debug('Sending to AI', { messageCount: history.length, sessionId });
process.stdin.pause();
const display = new StreamDisplay('assistant');
try {
const result = await callAI(provider, trimmed, cwd, model, sessionId, display);
if (result.sessionId) {
sessionId = result.sessionId;
updateAgentSession(cwd, agentName, sessionId);
}
const result = await callAIWithRetry(trimmed);
if (result) {
history.push({ role: 'assistant', content: result.content });
console.log();
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
log.error('AI call failed', { error: msg });
console.log();
console.log(chalk.red(`Error: ${msg}`));
console.log();
} else {
history.pop();
}
}

View File

@ -12,15 +12,16 @@ import type { AgentResponse } from '../models/types.js';
export async function withAgentSession(
cwd: string,
agentName: string,
fn: (sessionId?: string) => Promise<AgentResponse>
fn: (sessionId?: string) => Promise<AgentResponse>,
provider?: string
): Promise<AgentResponse> {
const sessions = loadAgentSessions(cwd);
const sessions = loadAgentSessions(cwd, provider);
const sessionId = sessions[agentName];
const result = await fn(sessionId);
if (result.sessionId) {
updateAgentSession(cwd, agentName, result.sessionId);
updateAgentSession(cwd, agentName, result.sessionId, provider);
}
return result;

View File

@ -12,6 +12,7 @@ import {
loadWorktreeSessions,
updateWorktreeSession,
} from '../config/paths.js';
import { loadGlobalConfig } from '../config/globalConfig.js';
import {
header,
info,
@ -131,9 +132,10 @@ export async function executeWorkflow(
// Load saved agent sessions for continuity (from project root or clone-specific storage)
const isWorktree = cwd !== projectCwd;
const currentProvider = loadGlobalConfig().provider ?? 'claude';
const savedSessions = isWorktree
? loadWorktreeSessions(projectCwd, cwd)
: loadAgentSessions(projectCwd);
: loadAgentSessions(projectCwd, currentProvider);
// Session update handler - persist session IDs when they change
// Clone sessions are stored separately per clone path
@ -142,7 +144,7 @@ export async function executeWorkflow(
updateWorktreeSession(projectCwd, cwd, agentName, agentSessionId);
}
: (agentName: string, agentSessionId: string): void => {
updateAgentSession(projectCwd, agentName, agentSessionId);
updateAgentSession(projectCwd, agentName, agentSessionId, currentProvider);
};
const iterationLimitHandler = async (

View File

@ -37,6 +37,8 @@ export function loadGlobalConfig(): GlobalConfig {
} : undefined,
worktreeDir: parsed.worktree_dir,
disabledBuiltins: parsed.disabled_builtins,
anthropicApiKey: parsed.anthropic_api_key,
openaiApiKey: parsed.openai_api_key,
};
}
@ -65,6 +67,12 @@ export function saveGlobalConfig(config: GlobalConfig): void {
if (config.disabledBuiltins && config.disabledBuiltins.length > 0) {
raw.disabled_builtins = config.disabledBuiltins;
}
if (config.anthropicApiKey) {
raw.anthropic_api_key = config.anthropicApiKey;
}
if (config.openaiApiKey) {
raw.openai_api_key = config.openaiApiKey;
}
writeFileSync(configPath, stringifyYaml(raw), 'utf-8');
}
@ -121,6 +129,38 @@ export function isDirectoryTrusted(dir: string): boolean {
);
}
/**
* Resolve the Anthropic API key.
* Priority: TAKT_ANTHROPIC_API_KEY env var > config.yaml > undefined (CLI auth fallback)
*/
export function resolveAnthropicApiKey(): string | undefined {
const envKey = process.env['TAKT_ANTHROPIC_API_KEY'];
if (envKey) return envKey;
try {
const config = loadGlobalConfig();
return config.anthropicApiKey;
} catch {
return undefined;
}
}
/**
* Resolve the OpenAI API key.
* Priority: TAKT_OPENAI_API_KEY env var > config.yaml > undefined (CLI auth fallback)
*/
export function resolveOpenaiApiKey(): string | undefined {
const envKey = process.env['TAKT_OPENAI_API_KEY'];
if (envKey) return envKey;
try {
const config = loadGlobalConfig();
return config.openaiApiKey;
} catch {
return undefined;
}
}
/** Load project-level debug configuration (from .takt/config.yaml) */
export function loadProjectDebugConfig(projectDir: string): DebugConfig | undefined {
const configPath = getProjectConfigPath(projectDir);

View File

@ -88,6 +88,8 @@ export function addToInputHistory(projectDir: string, input: string): void {
export interface AgentSessionData {
agentSessions: Record<string, string>;
updatedAt: string;
/** Provider that created these sessions (claude, codex, etc.) */
provider?: string;
}
/** Get path for storing agent sessions */
@ -95,13 +97,17 @@ export function getAgentSessionsPath(projectDir: string): string {
return join(getProjectConfigDir(projectDir), 'agent_sessions.json');
}
/** Load saved agent sessions */
export function loadAgentSessions(projectDir: string): Record<string, string> {
/** Load saved agent sessions. Returns empty if provider has changed. */
export function loadAgentSessions(projectDir: string, currentProvider?: string): Record<string, string> {
const path = getAgentSessionsPath(projectDir);
if (existsSync(path)) {
try {
const content = readFileSync(path, 'utf-8');
const data = JSON.parse(content) as AgentSessionData;
// If provider has changed or is unknown (legacy data), sessions are incompatible — discard them
if (currentProvider && data.provider !== currentProvider) {
return {};
}
return data.agentSessions || {};
} catch {
return {};
@ -113,13 +119,15 @@ export function loadAgentSessions(projectDir: string): Record<string, string> {
/** Save agent sessions (atomic write) */
export function saveAgentSessions(
projectDir: string,
sessions: Record<string, string>
sessions: Record<string, string>,
provider?: string
): void {
const path = getAgentSessionsPath(projectDir);
ensureDir(getProjectConfigDir(projectDir));
const data: AgentSessionData = {
agentSessions: sessions,
updatedAt: new Date().toISOString(),
provider,
};
writeFileAtomic(path, JSON.stringify(data, null, 2));
}
@ -131,17 +139,25 @@ export function saveAgentSessions(
export function updateAgentSession(
projectDir: string,
agentName: string,
sessionId: string
sessionId: string,
provider?: string
): void {
const path = getAgentSessionsPath(projectDir);
ensureDir(getProjectConfigDir(projectDir));
let sessions: Record<string, string> = {};
let existingProvider: string | undefined;
if (existsSync(path)) {
try {
const content = readFileSync(path, 'utf-8');
const data = JSON.parse(content) as AgentSessionData;
existingProvider = data.provider;
// If provider changed, discard old sessions
if (provider && existingProvider && existingProvider !== provider) {
sessions = {};
} else {
sessions = data.agentSessions || {};
}
} catch {
sessions = {};
}
@ -152,6 +168,7 @@ export function updateAgentSession(
const data: AgentSessionData = {
agentSessions: sessions,
updatedAt: new Date().toISOString(),
provider: provider ?? existingProvider,
};
writeFileAtomic(path, JSON.stringify(data, null, 2));
}

View File

@ -172,6 +172,10 @@ export const GlobalConfigSchema = z.object({
worktree_dir: z.string().optional(),
/** List of builtin workflow/agent names to exclude from fallback loading */
disabled_builtins: z.array(z.string()).optional().default([]),
/** Anthropic API key for Claude Code SDK (overridden by TAKT_ANTHROPIC_API_KEY env var) */
anthropic_api_key: z.string().optional(),
/** OpenAI API key for Codex SDK (overridden by TAKT_OPENAI_API_KEY env var) */
openai_api_key: z.string().optional(),
});
/** Project config schema */

View File

@ -191,6 +191,10 @@ export interface GlobalConfig {
worktreeDir?: string;
/** List of builtin workflow/agent names to exclude from fallback loading */
disabledBuiltins?: string[];
/** Anthropic API key for Claude Code SDK (overridden by TAKT_ANTHROPIC_API_KEY env var) */
anthropicApiKey?: string;
/** OpenAI API key for Codex SDK (overridden by TAKT_OPENAI_API_KEY env var) */
openaiApiKey?: string;
}
/** Project-level configuration */

View File

@ -3,6 +3,7 @@
*/
import { callClaude, callClaudeCustom, type ClaudeCallOptions } from '../claude/client.js';
import { resolveAnthropicApiKey } from '../config/globalConfig.js';
import type { AgentResponse } from '../models/types.js';
import type { Provider, ProviderCallOptions } from './index.js';
@ -21,6 +22,7 @@ export class ClaudeProvider implements Provider {
onPermissionRequest: options.onPermissionRequest,
onAskUserQuestion: options.onAskUserQuestion,
bypassPermissions: options.bypassPermissions,
anthropicApiKey: options.anthropicApiKey ?? resolveAnthropicApiKey(),
};
return callClaude(agentName, prompt, callOptions);
@ -38,6 +40,7 @@ export class ClaudeProvider implements Provider {
onPermissionRequest: options.onPermissionRequest,
onAskUserQuestion: options.onAskUserQuestion,
bypassPermissions: options.bypassPermissions,
anthropicApiKey: options.anthropicApiKey ?? resolveAnthropicApiKey(),
};
return callClaudeCustom(agentName, prompt, systemPrompt, callOptions);

View File

@ -3,6 +3,7 @@
*/
import { callCodex, callCodexCustom, type CodexCallOptions } from '../codex/client.js';
import { resolveOpenaiApiKey } from '../config/globalConfig.js';
import type { AgentResponse } from '../models/types.js';
import type { Provider, ProviderCallOptions } from './index.js';
@ -15,6 +16,7 @@ export class CodexProvider implements Provider {
model: options.model,
systemPrompt: options.systemPrompt,
onStream: options.onStream,
openaiApiKey: options.openaiApiKey ?? resolveOpenaiApiKey(),
};
return callCodex(agentName, prompt, callOptions);
@ -26,6 +28,7 @@ export class CodexProvider implements Provider {
sessionId: options.sessionId,
model: options.model,
onStream: options.onStream,
openaiApiKey: options.openaiApiKey ?? resolveOpenaiApiKey(),
};
return callCodexCustom(agentName, prompt, systemPrompt, callOptions);

View File

@ -26,6 +26,10 @@ export interface ProviderCallOptions {
onPermissionRequest?: PermissionHandler;
onAskUserQuestion?: AskUserQuestionHandler;
bypassPermissions?: boolean;
/** Anthropic API key for Claude provider */
anthropicApiKey?: string;
/** OpenAI API key for Codex provider */
openaiApiKey?: string;
}
/** Provider interface - all providers must implement this */

View File

@ -327,7 +327,7 @@ export class StreamDisplay {
}
/** Display final result */
showResult(success: boolean): void {
showResult(success: boolean, error?: string): void {
this.stopToolSpinner();
this.flushThinking();
this.flushText();
@ -336,6 +336,9 @@ export class StreamDisplay {
console.log(chalk.green('✓ Complete'));
} else {
console.log(chalk.red('✗ Failed'));
if (error) {
console.log(chalk.red(` ${error}`));
}
}
}
@ -378,7 +381,7 @@ export class StreamDisplay {
this.showThinking(event.data.thinking);
break;
case 'result':
this.showResult(event.data.success);
this.showResult(event.data.success, event.data.error);
break;
case 'error':
// Parse errors are logged but not displayed to user