resolved #35
This commit is contained in:
parent
6468fa6345
commit
4b924851a8
@ -21,6 +21,12 @@ provider: claude
|
|||||||
# Codex: gpt-5.2-codex, gpt-5.1-codex, etc.
|
# Codex: gpt-5.2-codex, gpt-5.1-codex, etc.
|
||||||
# model: sonnet
|
# 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 settings (optional)
|
||||||
# debug:
|
# debug:
|
||||||
# enabled: false
|
# enabled: false
|
||||||
|
|||||||
@ -21,6 +21,12 @@ provider: claude
|
|||||||
# Codex: gpt-5.2-codex, gpt-5.1-codex など
|
# Codex: gpt-5.2-codex, gpt-5.1-codex など
|
||||||
# model: sonnet
|
# model: sonnet
|
||||||
|
|
||||||
|
# Anthropic APIキー (オプション、環境変数 TAKT_ANTHROPIC_API_KEY で上書き可能)
|
||||||
|
# anthropic_api_key: ""
|
||||||
|
|
||||||
|
# OpenAI APIキー (オプション、環境変数 TAKT_OPENAI_API_KEY で上書き可能)
|
||||||
|
# openai_api_key: ""
|
||||||
|
|
||||||
# デバッグ設定 (オプション)
|
# デバッグ設定 (オプション)
|
||||||
# debug:
|
# debug:
|
||||||
# enabled: false
|
# enabled: false
|
||||||
|
|||||||
289
src/__tests__/apiKeyAuth.test.ts
Normal file
289
src/__tests__/apiKeyAuth.test.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -31,6 +31,8 @@ export interface ClaudeCallOptions {
|
|||||||
onAskUserQuestion?: AskUserQuestionHandler;
|
onAskUserQuestion?: AskUserQuestionHandler;
|
||||||
/** Bypass all permission checks (sacrifice-my-pc mode) */
|
/** Bypass all permission checks (sacrifice-my-pc mode) */
|
||||||
bypassPermissions?: boolean;
|
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,
|
onPermissionRequest: options.onPermissionRequest,
|
||||||
onAskUserQuestion: options.onAskUserQuestion,
|
onAskUserQuestion: options.onAskUserQuestion,
|
||||||
bypassPermissions: options.bypassPermissions,
|
bypassPermissions: options.bypassPermissions,
|
||||||
|
anthropicApiKey: options.anthropicApiKey,
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await executeClaudeCli(prompt, spawnOptions);
|
const result = await executeClaudeCli(prompt, spawnOptions);
|
||||||
@ -146,6 +149,7 @@ export async function callClaudeCustom(
|
|||||||
onPermissionRequest: options.onPermissionRequest,
|
onPermissionRequest: options.onPermissionRequest,
|
||||||
onAskUserQuestion: options.onAskUserQuestion,
|
onAskUserQuestion: options.onAskUserQuestion,
|
||||||
bypassPermissions: options.bypassPermissions,
|
bypassPermissions: options.bypassPermissions,
|
||||||
|
anthropicApiKey: options.anthropicApiKey,
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await executeClaudeCli(prompt, spawnOptions);
|
const result = await executeClaudeCli(prompt, spawnOptions);
|
||||||
@ -272,6 +276,7 @@ export async function callClaudeSkill(
|
|||||||
onPermissionRequest: options.onPermissionRequest,
|
onPermissionRequest: options.onPermissionRequest,
|
||||||
onAskUserQuestion: options.onAskUserQuestion,
|
onAskUserQuestion: options.onAskUserQuestion,
|
||||||
bypassPermissions: options.bypassPermissions,
|
bypassPermissions: options.bypassPermissions,
|
||||||
|
anthropicApiKey: options.anthropicApiKey,
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await executeClaudeCli(fullPrompt, spawnOptions);
|
const result = await executeClaudeCli(fullPrompt, spawnOptions);
|
||||||
|
|||||||
@ -25,6 +25,7 @@ import {
|
|||||||
createCanUseToolCallback,
|
createCanUseToolCallback,
|
||||||
createAskUserQuestionHooks,
|
createAskUserQuestionHooks,
|
||||||
} from './options-builder.js';
|
} from './options-builder.js';
|
||||||
|
import { resolveAnthropicApiKey } from '../config/globalConfig.js';
|
||||||
import type {
|
import type {
|
||||||
StreamCallback,
|
StreamCallback,
|
||||||
PermissionHandler,
|
PermissionHandler,
|
||||||
@ -49,6 +50,8 @@ export interface ExecuteOptions {
|
|||||||
onAskUserQuestion?: AskUserQuestionHandler;
|
onAskUserQuestion?: AskUserQuestionHandler;
|
||||||
/** Bypass all permission checks (sacrifice-my-pc mode) */
|
/** Bypass all permission checks (sacrifice-my-pc mode) */
|
||||||
bypassPermissions?: boolean;
|
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 (canUseTool) sdkOptions.canUseTool = canUseTool;
|
||||||
if (hooks) sdkOptions.hooks = hooks;
|
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) {
|
if (options.onStream) {
|
||||||
sdkOptions.includePartialMessages = true;
|
sdkOptions.includePartialMessages = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -69,6 +69,8 @@ export interface ClaudeSpawnOptions {
|
|||||||
onAskUserQuestion?: AskUserQuestionHandler;
|
onAskUserQuestion?: AskUserQuestionHandler;
|
||||||
/** Bypass all permission checks (sacrifice-my-pc mode) */
|
/** Bypass all permission checks (sacrifice-my-pc mode) */
|
||||||
bypassPermissions?: boolean;
|
bypassPermissions?: boolean;
|
||||||
|
/** Anthropic API key to inject via env (bypasses CLI auth) */
|
||||||
|
anthropicApiKey?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -154,12 +154,16 @@ export function sdkMessageToStreamEvent(
|
|||||||
|
|
||||||
case 'result': {
|
case 'result': {
|
||||||
const resultMsg = message as SDKResultMessage;
|
const resultMsg = message as SDKResultMessage;
|
||||||
|
const errors = resultMsg.subtype !== 'success' && resultMsg.errors?.length
|
||||||
|
? resultMsg.errors.join('\n')
|
||||||
|
: undefined;
|
||||||
callback({
|
callback({
|
||||||
type: 'result',
|
type: 'result',
|
||||||
data: {
|
data: {
|
||||||
result: resultMsg.subtype === 'success' ? resultMsg.result : '',
|
result: resultMsg.subtype === 'success' ? resultMsg.result : '',
|
||||||
sessionId: resultMsg.session_id,
|
sessionId: resultMsg.session_id,
|
||||||
success: resultMsg.subtype === 'success',
|
success: resultMsg.subtype === 'success',
|
||||||
|
error: errors,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|||||||
@ -44,6 +44,7 @@ export interface ResultEventData {
|
|||||||
result: string;
|
result: string;
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
success: boolean;
|
success: boolean;
|
||||||
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ErrorEventData {
|
export interface ErrorEventData {
|
||||||
|
|||||||
@ -19,6 +19,8 @@ export interface CodexCallOptions {
|
|||||||
systemPrompt?: string;
|
systemPrompt?: string;
|
||||||
/** Enable streaming mode with callback (best-effort) */
|
/** Enable streaming mode with callback (best-effort) */
|
||||||
onStream?: StreamCallback;
|
onStream?: StreamCallback;
|
||||||
|
/** OpenAI API key (bypasses CLI auth) */
|
||||||
|
openaiApiKey?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractThreadId(value: unknown): string | undefined {
|
function extractThreadId(value: unknown): string | undefined {
|
||||||
@ -94,6 +96,7 @@ function emitResult(
|
|||||||
result,
|
result,
|
||||||
sessionId: sessionId || 'unknown',
|
sessionId: sessionId || 'unknown',
|
||||||
success,
|
success,
|
||||||
|
error: success ? undefined : result || undefined,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -341,7 +344,7 @@ export async function callCodex(
|
|||||||
prompt: string,
|
prompt: string,
|
||||||
options: CodexCallOptions
|
options: CodexCallOptions
|
||||||
): Promise<AgentResponse> {
|
): Promise<AgentResponse> {
|
||||||
const codex = new Codex();
|
const codex = new Codex(options.openaiApiKey ? { apiKey: options.openaiApiKey } : undefined);
|
||||||
const threadOptions = {
|
const threadOptions = {
|
||||||
model: options.model,
|
model: options.model,
|
||||||
workingDirectory: options.cwd,
|
workingDirectory: options.cwd,
|
||||||
|
|||||||
@ -44,6 +44,7 @@ interface ConversationMessage {
|
|||||||
interface CallAIResult {
|
interface CallAIResult {
|
||||||
content: string;
|
content: string;
|
||||||
sessionId?: string;
|
sessionId?: string;
|
||||||
|
success: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -111,7 +112,8 @@ async function callAI(
|
|||||||
});
|
});
|
||||||
|
|
||||||
display.flush();
|
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 {
|
export interface InteractiveModeResult {
|
||||||
@ -138,7 +140,7 @@ export async function interactiveMode(cwd: string, initialInput?: string): Promi
|
|||||||
|
|
||||||
const history: ConversationMessage[] = [];
|
const history: ConversationMessage[] = [];
|
||||||
const agentName = 'interactive';
|
const agentName = 'interactive';
|
||||||
const savedSessions = loadAgentSessions(cwd);
|
const savedSessions = loadAgentSessions(cwd, providerType);
|
||||||
let sessionId: string | undefined = savedSessions[agentName];
|
let sessionId: string | undefined = savedSessions[agentName];
|
||||||
|
|
||||||
info('Interactive mode - describe your task. Commands: /go (execute), /cancel (exit)');
|
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();
|
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`)
|
// Process initial input if provided (e.g. from `takt a`)
|
||||||
if (initialInput) {
|
if (initialInput) {
|
||||||
history.push({ role: 'user', content: initialInput });
|
history.push({ role: 'user', content: initialInput });
|
||||||
|
|
||||||
log.debug('Processing initial input', { initialInput, sessionId });
|
log.debug('Processing initial input', { initialInput, sessionId });
|
||||||
|
|
||||||
const display = new StreamDisplay('assistant');
|
const result = await callAIWithRetry(initialInput);
|
||||||
try {
|
if (result) {
|
||||||
const result = await callAI(provider, initialInput, cwd, model, sessionId, display);
|
|
||||||
if (result.sessionId) {
|
|
||||||
sessionId = result.sessionId;
|
|
||||||
updateAgentSession(cwd, agentName, sessionId);
|
|
||||||
}
|
|
||||||
history.push({ role: 'assistant', content: result.content });
|
history.push({ role: 'assistant', content: result.content });
|
||||||
console.log();
|
console.log();
|
||||||
} catch (e) {
|
} else {
|
||||||
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();
|
|
||||||
history.pop();
|
history.pop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -211,21 +234,11 @@ export async function interactiveMode(cwd: string, initialInput?: string): Promi
|
|||||||
log.debug('Sending to AI', { messageCount: history.length, sessionId });
|
log.debug('Sending to AI', { messageCount: history.length, sessionId });
|
||||||
process.stdin.pause();
|
process.stdin.pause();
|
||||||
|
|
||||||
const display = new StreamDisplay('assistant');
|
const result = await callAIWithRetry(trimmed);
|
||||||
try {
|
if (result) {
|
||||||
const result = await callAI(provider, trimmed, cwd, model, sessionId, display);
|
|
||||||
if (result.sessionId) {
|
|
||||||
sessionId = result.sessionId;
|
|
||||||
updateAgentSession(cwd, agentName, sessionId);
|
|
||||||
}
|
|
||||||
history.push({ role: 'assistant', content: result.content });
|
history.push({ role: 'assistant', content: result.content });
|
||||||
console.log();
|
console.log();
|
||||||
} catch (e) {
|
} else {
|
||||||
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();
|
|
||||||
history.pop();
|
history.pop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,15 +12,16 @@ import type { AgentResponse } from '../models/types.js';
|
|||||||
export async function withAgentSession(
|
export async function withAgentSession(
|
||||||
cwd: string,
|
cwd: string,
|
||||||
agentName: string,
|
agentName: string,
|
||||||
fn: (sessionId?: string) => Promise<AgentResponse>
|
fn: (sessionId?: string) => Promise<AgentResponse>,
|
||||||
|
provider?: string
|
||||||
): Promise<AgentResponse> {
|
): Promise<AgentResponse> {
|
||||||
const sessions = loadAgentSessions(cwd);
|
const sessions = loadAgentSessions(cwd, provider);
|
||||||
const sessionId = sessions[agentName];
|
const sessionId = sessions[agentName];
|
||||||
|
|
||||||
const result = await fn(sessionId);
|
const result = await fn(sessionId);
|
||||||
|
|
||||||
if (result.sessionId) {
|
if (result.sessionId) {
|
||||||
updateAgentSession(cwd, agentName, result.sessionId);
|
updateAgentSession(cwd, agentName, result.sessionId, provider);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import {
|
|||||||
loadWorktreeSessions,
|
loadWorktreeSessions,
|
||||||
updateWorktreeSession,
|
updateWorktreeSession,
|
||||||
} from '../config/paths.js';
|
} from '../config/paths.js';
|
||||||
|
import { loadGlobalConfig } from '../config/globalConfig.js';
|
||||||
import {
|
import {
|
||||||
header,
|
header,
|
||||||
info,
|
info,
|
||||||
@ -131,9 +132,10 @@ export async function executeWorkflow(
|
|||||||
|
|
||||||
// Load saved agent sessions for continuity (from project root or clone-specific storage)
|
// Load saved agent sessions for continuity (from project root or clone-specific storage)
|
||||||
const isWorktree = cwd !== projectCwd;
|
const isWorktree = cwd !== projectCwd;
|
||||||
|
const currentProvider = loadGlobalConfig().provider ?? 'claude';
|
||||||
const savedSessions = isWorktree
|
const savedSessions = isWorktree
|
||||||
? loadWorktreeSessions(projectCwd, cwd)
|
? loadWorktreeSessions(projectCwd, cwd)
|
||||||
: loadAgentSessions(projectCwd);
|
: loadAgentSessions(projectCwd, currentProvider);
|
||||||
|
|
||||||
// Session update handler - persist session IDs when they change
|
// Session update handler - persist session IDs when they change
|
||||||
// Clone sessions are stored separately per clone path
|
// Clone sessions are stored separately per clone path
|
||||||
@ -142,7 +144,7 @@ export async function executeWorkflow(
|
|||||||
updateWorktreeSession(projectCwd, cwd, agentName, agentSessionId);
|
updateWorktreeSession(projectCwd, cwd, agentName, agentSessionId);
|
||||||
}
|
}
|
||||||
: (agentName: string, agentSessionId: string): void => {
|
: (agentName: string, agentSessionId: string): void => {
|
||||||
updateAgentSession(projectCwd, agentName, agentSessionId);
|
updateAgentSession(projectCwd, agentName, agentSessionId, currentProvider);
|
||||||
};
|
};
|
||||||
|
|
||||||
const iterationLimitHandler = async (
|
const iterationLimitHandler = async (
|
||||||
|
|||||||
@ -37,6 +37,8 @@ export function loadGlobalConfig(): GlobalConfig {
|
|||||||
} : undefined,
|
} : undefined,
|
||||||
worktreeDir: parsed.worktree_dir,
|
worktreeDir: parsed.worktree_dir,
|
||||||
disabledBuiltins: parsed.disabled_builtins,
|
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) {
|
if (config.disabledBuiltins && config.disabledBuiltins.length > 0) {
|
||||||
raw.disabled_builtins = config.disabledBuiltins;
|
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');
|
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) */
|
/** Load project-level debug configuration (from .takt/config.yaml) */
|
||||||
export function loadProjectDebugConfig(projectDir: string): DebugConfig | undefined {
|
export function loadProjectDebugConfig(projectDir: string): DebugConfig | undefined {
|
||||||
const configPath = getProjectConfigPath(projectDir);
|
const configPath = getProjectConfigPath(projectDir);
|
||||||
|
|||||||
@ -88,6 +88,8 @@ export function addToInputHistory(projectDir: string, input: string): void {
|
|||||||
export interface AgentSessionData {
|
export interface AgentSessionData {
|
||||||
agentSessions: Record<string, string>;
|
agentSessions: Record<string, string>;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
|
/** Provider that created these sessions (claude, codex, etc.) */
|
||||||
|
provider?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Get path for storing agent sessions */
|
/** Get path for storing agent sessions */
|
||||||
@ -95,13 +97,17 @@ export function getAgentSessionsPath(projectDir: string): string {
|
|||||||
return join(getProjectConfigDir(projectDir), 'agent_sessions.json');
|
return join(getProjectConfigDir(projectDir), 'agent_sessions.json');
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Load saved agent sessions */
|
/** Load saved agent sessions. Returns empty if provider has changed. */
|
||||||
export function loadAgentSessions(projectDir: string): Record<string, string> {
|
export function loadAgentSessions(projectDir: string, currentProvider?: string): Record<string, string> {
|
||||||
const path = getAgentSessionsPath(projectDir);
|
const path = getAgentSessionsPath(projectDir);
|
||||||
if (existsSync(path)) {
|
if (existsSync(path)) {
|
||||||
try {
|
try {
|
||||||
const content = readFileSync(path, 'utf-8');
|
const content = readFileSync(path, 'utf-8');
|
||||||
const data = JSON.parse(content) as AgentSessionData;
|
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 || {};
|
return data.agentSessions || {};
|
||||||
} catch {
|
} catch {
|
||||||
return {};
|
return {};
|
||||||
@ -113,13 +119,15 @@ export function loadAgentSessions(projectDir: string): Record<string, string> {
|
|||||||
/** Save agent sessions (atomic write) */
|
/** Save agent sessions (atomic write) */
|
||||||
export function saveAgentSessions(
|
export function saveAgentSessions(
|
||||||
projectDir: string,
|
projectDir: string,
|
||||||
sessions: Record<string, string>
|
sessions: Record<string, string>,
|
||||||
|
provider?: string
|
||||||
): void {
|
): void {
|
||||||
const path = getAgentSessionsPath(projectDir);
|
const path = getAgentSessionsPath(projectDir);
|
||||||
ensureDir(getProjectConfigDir(projectDir));
|
ensureDir(getProjectConfigDir(projectDir));
|
||||||
const data: AgentSessionData = {
|
const data: AgentSessionData = {
|
||||||
agentSessions: sessions,
|
agentSessions: sessions,
|
||||||
updatedAt: new Date().toISOString(),
|
updatedAt: new Date().toISOString(),
|
||||||
|
provider,
|
||||||
};
|
};
|
||||||
writeFileAtomic(path, JSON.stringify(data, null, 2));
|
writeFileAtomic(path, JSON.stringify(data, null, 2));
|
||||||
}
|
}
|
||||||
@ -131,17 +139,25 @@ export function saveAgentSessions(
|
|||||||
export function updateAgentSession(
|
export function updateAgentSession(
|
||||||
projectDir: string,
|
projectDir: string,
|
||||||
agentName: string,
|
agentName: string,
|
||||||
sessionId: string
|
sessionId: string,
|
||||||
|
provider?: string
|
||||||
): void {
|
): void {
|
||||||
const path = getAgentSessionsPath(projectDir);
|
const path = getAgentSessionsPath(projectDir);
|
||||||
ensureDir(getProjectConfigDir(projectDir));
|
ensureDir(getProjectConfigDir(projectDir));
|
||||||
|
|
||||||
let sessions: Record<string, string> = {};
|
let sessions: Record<string, string> = {};
|
||||||
|
let existingProvider: string | undefined;
|
||||||
if (existsSync(path)) {
|
if (existsSync(path)) {
|
||||||
try {
|
try {
|
||||||
const content = readFileSync(path, 'utf-8');
|
const content = readFileSync(path, 'utf-8');
|
||||||
const data = JSON.parse(content) as AgentSessionData;
|
const data = JSON.parse(content) as AgentSessionData;
|
||||||
sessions = data.agentSessions || {};
|
existingProvider = data.provider;
|
||||||
|
// If provider changed, discard old sessions
|
||||||
|
if (provider && existingProvider && existingProvider !== provider) {
|
||||||
|
sessions = {};
|
||||||
|
} else {
|
||||||
|
sessions = data.agentSessions || {};
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
sessions = {};
|
sessions = {};
|
||||||
}
|
}
|
||||||
@ -152,6 +168,7 @@ export function updateAgentSession(
|
|||||||
const data: AgentSessionData = {
|
const data: AgentSessionData = {
|
||||||
agentSessions: sessions,
|
agentSessions: sessions,
|
||||||
updatedAt: new Date().toISOString(),
|
updatedAt: new Date().toISOString(),
|
||||||
|
provider: provider ?? existingProvider,
|
||||||
};
|
};
|
||||||
writeFileAtomic(path, JSON.stringify(data, null, 2));
|
writeFileAtomic(path, JSON.stringify(data, null, 2));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -172,6 +172,10 @@ export const GlobalConfigSchema = z.object({
|
|||||||
worktree_dir: z.string().optional(),
|
worktree_dir: z.string().optional(),
|
||||||
/** List of builtin workflow/agent names to exclude from fallback loading */
|
/** List of builtin workflow/agent names to exclude from fallback loading */
|
||||||
disabled_builtins: z.array(z.string()).optional().default([]),
|
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 */
|
/** Project config schema */
|
||||||
|
|||||||
@ -191,6 +191,10 @@ export interface GlobalConfig {
|
|||||||
worktreeDir?: string;
|
worktreeDir?: string;
|
||||||
/** List of builtin workflow/agent names to exclude from fallback loading */
|
/** List of builtin workflow/agent names to exclude from fallback loading */
|
||||||
disabledBuiltins?: string[];
|
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 */
|
/** Project-level configuration */
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { callClaude, callClaudeCustom, type ClaudeCallOptions } from '../claude/client.js';
|
import { callClaude, callClaudeCustom, type ClaudeCallOptions } from '../claude/client.js';
|
||||||
|
import { resolveAnthropicApiKey } from '../config/globalConfig.js';
|
||||||
import type { AgentResponse } from '../models/types.js';
|
import type { AgentResponse } from '../models/types.js';
|
||||||
import type { Provider, ProviderCallOptions } from './index.js';
|
import type { Provider, ProviderCallOptions } from './index.js';
|
||||||
|
|
||||||
@ -21,6 +22,7 @@ export class ClaudeProvider implements Provider {
|
|||||||
onPermissionRequest: options.onPermissionRequest,
|
onPermissionRequest: options.onPermissionRequest,
|
||||||
onAskUserQuestion: options.onAskUserQuestion,
|
onAskUserQuestion: options.onAskUserQuestion,
|
||||||
bypassPermissions: options.bypassPermissions,
|
bypassPermissions: options.bypassPermissions,
|
||||||
|
anthropicApiKey: options.anthropicApiKey ?? resolveAnthropicApiKey(),
|
||||||
};
|
};
|
||||||
|
|
||||||
return callClaude(agentName, prompt, callOptions);
|
return callClaude(agentName, prompt, callOptions);
|
||||||
@ -38,6 +40,7 @@ export class ClaudeProvider implements Provider {
|
|||||||
onPermissionRequest: options.onPermissionRequest,
|
onPermissionRequest: options.onPermissionRequest,
|
||||||
onAskUserQuestion: options.onAskUserQuestion,
|
onAskUserQuestion: options.onAskUserQuestion,
|
||||||
bypassPermissions: options.bypassPermissions,
|
bypassPermissions: options.bypassPermissions,
|
||||||
|
anthropicApiKey: options.anthropicApiKey ?? resolveAnthropicApiKey(),
|
||||||
};
|
};
|
||||||
|
|
||||||
return callClaudeCustom(agentName, prompt, systemPrompt, callOptions);
|
return callClaudeCustom(agentName, prompt, systemPrompt, callOptions);
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { callCodex, callCodexCustom, type CodexCallOptions } from '../codex/client.js';
|
import { callCodex, callCodexCustom, type CodexCallOptions } from '../codex/client.js';
|
||||||
|
import { resolveOpenaiApiKey } from '../config/globalConfig.js';
|
||||||
import type { AgentResponse } from '../models/types.js';
|
import type { AgentResponse } from '../models/types.js';
|
||||||
import type { Provider, ProviderCallOptions } from './index.js';
|
import type { Provider, ProviderCallOptions } from './index.js';
|
||||||
|
|
||||||
@ -15,6 +16,7 @@ export class CodexProvider implements Provider {
|
|||||||
model: options.model,
|
model: options.model,
|
||||||
systemPrompt: options.systemPrompt,
|
systemPrompt: options.systemPrompt,
|
||||||
onStream: options.onStream,
|
onStream: options.onStream,
|
||||||
|
openaiApiKey: options.openaiApiKey ?? resolveOpenaiApiKey(),
|
||||||
};
|
};
|
||||||
|
|
||||||
return callCodex(agentName, prompt, callOptions);
|
return callCodex(agentName, prompt, callOptions);
|
||||||
@ -26,6 +28,7 @@ export class CodexProvider implements Provider {
|
|||||||
sessionId: options.sessionId,
|
sessionId: options.sessionId,
|
||||||
model: options.model,
|
model: options.model,
|
||||||
onStream: options.onStream,
|
onStream: options.onStream,
|
||||||
|
openaiApiKey: options.openaiApiKey ?? resolveOpenaiApiKey(),
|
||||||
};
|
};
|
||||||
|
|
||||||
return callCodexCustom(agentName, prompt, systemPrompt, callOptions);
|
return callCodexCustom(agentName, prompt, systemPrompt, callOptions);
|
||||||
|
|||||||
@ -26,6 +26,10 @@ export interface ProviderCallOptions {
|
|||||||
onPermissionRequest?: PermissionHandler;
|
onPermissionRequest?: PermissionHandler;
|
||||||
onAskUserQuestion?: AskUserQuestionHandler;
|
onAskUserQuestion?: AskUserQuestionHandler;
|
||||||
bypassPermissions?: boolean;
|
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 */
|
/** Provider interface - all providers must implement this */
|
||||||
|
|||||||
@ -327,7 +327,7 @@ export class StreamDisplay {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Display final result */
|
/** Display final result */
|
||||||
showResult(success: boolean): void {
|
showResult(success: boolean, error?: string): void {
|
||||||
this.stopToolSpinner();
|
this.stopToolSpinner();
|
||||||
this.flushThinking();
|
this.flushThinking();
|
||||||
this.flushText();
|
this.flushText();
|
||||||
@ -336,6 +336,9 @@ export class StreamDisplay {
|
|||||||
console.log(chalk.green('✓ Complete'));
|
console.log(chalk.green('✓ Complete'));
|
||||||
} else {
|
} else {
|
||||||
console.log(chalk.red('✗ Failed'));
|
console.log(chalk.red('✗ Failed'));
|
||||||
|
if (error) {
|
||||||
|
console.log(chalk.red(` ${error}`));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -378,7 +381,7 @@ export class StreamDisplay {
|
|||||||
this.showThinking(event.data.thinking);
|
this.showThinking(event.data.thinking);
|
||||||
break;
|
break;
|
||||||
case 'result':
|
case 'result':
|
||||||
this.showResult(event.data.success);
|
this.showResult(event.data.success, event.data.error);
|
||||||
break;
|
break;
|
||||||
case 'error':
|
case 'error':
|
||||||
// Parse errors are logged but not displayed to user
|
// Parse errors are logged but not displayed to user
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user