commit
b0594c30e9
@ -6,6 +6,14 @@ All notable changes to this project will be documented in this file.
|
|||||||
|
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||||
|
|
||||||
|
## [0.18.2] - 2026-02-18
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added `codex_cli_path` global config option and `TAKT_CODEX_CLI_PATH` environment variable to override the Codex CLI binary path used by the Codex SDK (#292)
|
||||||
|
- Supports strict validation: absolute path, file existence, executable permission, no control characters
|
||||||
|
- Priority: `TAKT_CODEX_CLI_PATH` env var > `codex_cli_path` in config.yaml > SDK vendored binary
|
||||||
|
|
||||||
## [0.18.1] - 2026-02-18
|
## [0.18.1] - 2026-02-18
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@ -612,6 +612,11 @@ anthropic_api_key: sk-ant-... # For Claude (Anthropic)
|
|||||||
# openai_api_key: sk-... # For Codex (OpenAI)
|
# openai_api_key: sk-... # For Codex (OpenAI)
|
||||||
# opencode_api_key: ... # For OpenCode
|
# opencode_api_key: ... # For OpenCode
|
||||||
|
|
||||||
|
# Codex CLI path override (optional)
|
||||||
|
# Override the Codex CLI binary used by the Codex SDK (must be an absolute path to an executable file)
|
||||||
|
# Can be overridden by TAKT_CODEX_CLI_PATH environment variable
|
||||||
|
# codex_cli_path: /usr/local/bin/codex
|
||||||
|
|
||||||
# Builtin piece filtering (optional)
|
# Builtin piece filtering (optional)
|
||||||
# builtin_pieces_enabled: true # Set false to disable all builtins
|
# builtin_pieces_enabled: true # Set false to disable all builtins
|
||||||
# disabled_builtins: [magi, passthrough] # Disable specific builtin pieces
|
# disabled_builtins: [magi, passthrough] # Disable specific builtin pieces
|
||||||
|
|||||||
@ -6,6 +6,14 @@
|
|||||||
|
|
||||||
フォーマットは [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) に基づいています。
|
フォーマットは [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) に基づいています。
|
||||||
|
|
||||||
|
## [0.18.2] - 2026-02-18
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- グローバル設定に `codex_cli_path` オプションと `TAKT_CODEX_CLI_PATH` 環境変数を追加 — Codex SDK が使用する CLI バイナリのパスを上書き可能に (#292)
|
||||||
|
- 厳密なバリデーション付き: 絶対パス、ファイル存在確認、実行権限、制御文字の禁止
|
||||||
|
- 優先順位: `TAKT_CODEX_CLI_PATH` 環境変数 > config.yaml の `codex_cli_path` > SDK 同梱バイナリ
|
||||||
|
|
||||||
## [0.18.1] - 2026-02-18
|
## [0.18.1] - 2026-02-18
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@ -612,6 +612,11 @@ anthropic_api_key: sk-ant-... # Claude (Anthropic) を使う場合
|
|||||||
# openai_api_key: sk-... # Codex (OpenAI) を使う場合
|
# openai_api_key: sk-... # Codex (OpenAI) を使う場合
|
||||||
# opencode_api_key: ... # OpenCode を使う場合
|
# opencode_api_key: ... # OpenCode を使う場合
|
||||||
|
|
||||||
|
# Codex CLI パスの上書き(オプション)
|
||||||
|
# Codex SDK が使用する CLI バイナリを上書き(実行可能ファイルの絶対パスを指定)
|
||||||
|
# 環境変数 TAKT_CODEX_CLI_PATH で上書き可能
|
||||||
|
# codex_cli_path: /usr/local/bin/codex
|
||||||
|
|
||||||
# ビルトインピースのフィルタリング(オプション)
|
# ビルトインピースのフィルタリング(オプション)
|
||||||
# builtin_pieces_enabled: true # false でビルトイン全体を無効化
|
# builtin_pieces_enabled: true # false でビルトイン全体を無効化
|
||||||
# disabled_builtins: [magi, passthrough] # 特定のビルトインピースを無効化
|
# disabled_builtins: [magi, passthrough] # 特定のビルトインピースを無効化
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "takt",
|
"name": "takt",
|
||||||
"version": "0.18.1",
|
"version": "0.18.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "takt",
|
"name": "takt",
|
||||||
"version": "0.18.1",
|
"version": "0.18.2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/claude-agent-sdk": "^0.2.37",
|
"@anthropic-ai/claude-agent-sdk": "^0.2.37",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "takt",
|
"name": "takt",
|
||||||
"version": "0.18.1",
|
"version": "0.18.2",
|
||||||
"description": "TAKT: TAKT Agent Koordination Topology - AI Agent Piece Orchestration",
|
"description": "TAKT: TAKT Agent Koordination Topology - AI Agent Piece Orchestration",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
|
|||||||
@ -15,10 +15,14 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|||||||
|
|
||||||
let mockEvents: Array<Record<string, unknown>> = [];
|
let mockEvents: Array<Record<string, unknown>> = [];
|
||||||
let lastThreadOptions: Record<string, unknown> | undefined;
|
let lastThreadOptions: Record<string, unknown> | undefined;
|
||||||
|
let lastCodexConstructorOptions: Record<string, unknown> | undefined;
|
||||||
|
|
||||||
vi.mock('@openai/codex-sdk', () => {
|
vi.mock('@openai/codex-sdk', () => {
|
||||||
return {
|
return {
|
||||||
Codex: class MockCodex {
|
Codex: class MockCodex {
|
||||||
|
constructor(options?: Record<string, unknown>) {
|
||||||
|
lastCodexConstructorOptions = options;
|
||||||
|
}
|
||||||
async startThread(options?: Record<string, unknown>) {
|
async startThread(options?: Record<string, unknown>) {
|
||||||
lastThreadOptions = options;
|
lastThreadOptions = options;
|
||||||
return {
|
return {
|
||||||
@ -47,6 +51,7 @@ describe('CodexClient — structuredOutput 抽出', () => {
|
|||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
mockEvents = [];
|
mockEvents = [];
|
||||||
lastThreadOptions = undefined;
|
lastThreadOptions = undefined;
|
||||||
|
lastCodexConstructorOptions = undefined;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('outputSchema 指定時に agent_message の JSON テキストを structuredOutput として返す', async () => {
|
it('outputSchema 指定時に agent_message の JSON テキストを structuredOutput として返す', async () => {
|
||||||
@ -169,4 +174,21 @@ describe('CodexClient — structuredOutput 抽出', () => {
|
|||||||
networkAccessEnabled: true,
|
networkAccessEnabled: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('codexPathOverride が Codex constructor options に反映される', async () => {
|
||||||
|
mockEvents = [
|
||||||
|
{ type: 'thread.started', thread_id: 'thread-1' },
|
||||||
|
{ type: 'turn.completed', usage: { input_tokens: 0, cached_input_tokens: 0, output_tokens: 0 } },
|
||||||
|
];
|
||||||
|
|
||||||
|
const client = new CodexClient();
|
||||||
|
await client.call('coder', 'prompt', {
|
||||||
|
cwd: '/tmp',
|
||||||
|
codexPathOverride: '/opt/codex/bin/codex',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(lastCodexConstructorOptions).toMatchObject({
|
||||||
|
codexPathOverride: '/opt/codex/bin/codex',
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,344 +1,469 @@
|
|||||||
/**
|
/**
|
||||||
* Tests for API key authentication feature
|
* Tests for API key authentication feature
|
||||||
*
|
*
|
||||||
* Tests the resolution logic for Anthropic and OpenAI API keys:
|
* Tests the resolution logic for Anthropic and OpenAI API keys:
|
||||||
* - Environment variable priority over config.yaml
|
* - Environment variable priority over config.yaml
|
||||||
* - Config.yaml fallback when env var is not set
|
* - Config.yaml fallback when env var is not set
|
||||||
* - Undefined when neither is set
|
* - Undefined when neither is set
|
||||||
* - Schema validation for API key fields
|
* - Schema validation for API key fields
|
||||||
* - GlobalConfig load/save round-trip with API keys
|
* - GlobalConfig load/save round-trip with API keys
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||||
import { mkdirSync, rmSync, writeFileSync, readFileSync } from 'node:fs';
|
import { mkdirSync, rmSync, writeFileSync, readFileSync, chmodSync } from 'node:fs';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import { tmpdir } from 'node:os';
|
import { tmpdir } from 'node:os';
|
||||||
import { randomUUID } from 'node:crypto';
|
import { randomUUID } from 'node:crypto';
|
||||||
import { GlobalConfigSchema } from '../core/models/index.js';
|
import { GlobalConfigSchema } from '../core/models/index.js';
|
||||||
|
|
||||||
// Mock paths module to redirect config to temp directory
|
// Mock paths module to redirect config to temp directory
|
||||||
const testId = randomUUID();
|
const testId = randomUUID();
|
||||||
const testDir = join(tmpdir(), `takt-api-key-test-${testId}`);
|
const testDir = join(tmpdir(), `takt-api-key-test-${testId}`);
|
||||||
const taktDir = join(testDir, '.takt');
|
const taktDir = join(testDir, '.takt');
|
||||||
const configPath = join(taktDir, 'config.yaml');
|
const configPath = join(taktDir, 'config.yaml');
|
||||||
|
|
||||||
vi.mock('../infra/config/paths.js', async (importOriginal) => {
|
function createExecutableFile(filename: string): string {
|
||||||
const original = await importOriginal() as Record<string, unknown>;
|
const filePath = join(testDir, filename);
|
||||||
return {
|
writeFileSync(filePath, '#!/bin/sh\necho codex\n', 'utf-8');
|
||||||
...original,
|
chmodSync(filePath, 0o755);
|
||||||
getGlobalConfigPath: () => configPath,
|
return filePath;
|
||||||
getTaktDir: () => taktDir,
|
}
|
||||||
};
|
|
||||||
});
|
function createNonExecutableFile(filename: string): string {
|
||||||
|
const filePath = join(testDir, filename);
|
||||||
// Import after mocking
|
writeFileSync(filePath, '#!/bin/sh\necho codex\n', 'utf-8');
|
||||||
const { loadGlobalConfig, saveGlobalConfig, resolveAnthropicApiKey, resolveOpenaiApiKey, resolveOpencodeApiKey, invalidateGlobalConfigCache } = await import('../infra/config/global/globalConfig.js');
|
chmodSync(filePath, 0o644);
|
||||||
|
return filePath;
|
||||||
describe('GlobalConfigSchema API key fields', () => {
|
}
|
||||||
it('should accept config without API keys', () => {
|
|
||||||
const result = GlobalConfigSchema.parse({
|
vi.mock('../infra/config/paths.js', async (importOriginal) => {
|
||||||
language: 'en',
|
const original = await importOriginal() as Record<string, unknown>;
|
||||||
});
|
return {
|
||||||
expect(result.anthropic_api_key).toBeUndefined();
|
...original,
|
||||||
expect(result.openai_api_key).toBeUndefined();
|
getGlobalConfigPath: () => configPath,
|
||||||
});
|
getTaktDir: () => taktDir,
|
||||||
|
};
|
||||||
it('should accept config with anthropic_api_key', () => {
|
});
|
||||||
const result = GlobalConfigSchema.parse({
|
|
||||||
language: 'en',
|
// Import after mocking
|
||||||
anthropic_api_key: 'sk-ant-test-key',
|
const { loadGlobalConfig, saveGlobalConfig, resolveAnthropicApiKey, resolveOpenaiApiKey, resolveCodexCliPath, resolveOpencodeApiKey, invalidateGlobalConfigCache } = await import('../infra/config/global/globalConfig.js');
|
||||||
});
|
|
||||||
expect(result.anthropic_api_key).toBe('sk-ant-test-key');
|
describe('GlobalConfigSchema API key fields', () => {
|
||||||
});
|
it('should accept config without API keys', () => {
|
||||||
|
const result = GlobalConfigSchema.parse({
|
||||||
it('should accept config with openai_api_key', () => {
|
language: 'en',
|
||||||
const result = GlobalConfigSchema.parse({
|
});
|
||||||
language: 'en',
|
expect(result.anthropic_api_key).toBeUndefined();
|
||||||
openai_api_key: 'sk-openai-test-key',
|
expect(result.openai_api_key).toBeUndefined();
|
||||||
});
|
});
|
||||||
expect(result.openai_api_key).toBe('sk-openai-test-key');
|
|
||||||
});
|
it('should accept config with anthropic_api_key', () => {
|
||||||
|
const result = GlobalConfigSchema.parse({
|
||||||
it('should accept config with both API keys', () => {
|
language: 'en',
|
||||||
const result = GlobalConfigSchema.parse({
|
anthropic_api_key: 'sk-ant-test-key',
|
||||||
language: 'en',
|
});
|
||||||
anthropic_api_key: 'sk-ant-key',
|
expect(result.anthropic_api_key).toBe('sk-ant-test-key');
|
||||||
openai_api_key: 'sk-openai-key',
|
});
|
||||||
});
|
|
||||||
expect(result.anthropic_api_key).toBe('sk-ant-key');
|
it('should accept config with openai_api_key', () => {
|
||||||
expect(result.openai_api_key).toBe('sk-openai-key');
|
const result = GlobalConfigSchema.parse({
|
||||||
});
|
language: 'en',
|
||||||
});
|
openai_api_key: 'sk-openai-test-key',
|
||||||
|
});
|
||||||
describe('GlobalConfig load/save with API keys', () => {
|
expect(result.openai_api_key).toBe('sk-openai-test-key');
|
||||||
beforeEach(() => {
|
});
|
||||||
invalidateGlobalConfigCache();
|
|
||||||
mkdirSync(taktDir, { recursive: true });
|
it('should accept config with both API keys', () => {
|
||||||
});
|
const result = GlobalConfigSchema.parse({
|
||||||
|
language: 'en',
|
||||||
afterEach(() => {
|
anthropic_api_key: 'sk-ant-key',
|
||||||
rmSync(testDir, { recursive: true, force: true });
|
openai_api_key: 'sk-openai-key',
|
||||||
});
|
});
|
||||||
|
expect(result.anthropic_api_key).toBe('sk-ant-key');
|
||||||
it('should load config with API keys from YAML', () => {
|
expect(result.openai_api_key).toBe('sk-openai-key');
|
||||||
const yaml = [
|
});
|
||||||
'language: en',
|
});
|
||||||
'default_piece: default',
|
|
||||||
'log_level: info',
|
describe('GlobalConfig load/save with API keys', () => {
|
||||||
'provider: claude',
|
beforeEach(() => {
|
||||||
'anthropic_api_key: sk-ant-from-yaml',
|
invalidateGlobalConfigCache();
|
||||||
'openai_api_key: sk-openai-from-yaml',
|
mkdirSync(taktDir, { recursive: true });
|
||||||
].join('\n');
|
});
|
||||||
writeFileSync(configPath, yaml, 'utf-8');
|
|
||||||
|
afterEach(() => {
|
||||||
const config = loadGlobalConfig();
|
rmSync(testDir, { recursive: true, force: true });
|
||||||
expect(config.anthropicApiKey).toBe('sk-ant-from-yaml');
|
});
|
||||||
expect(config.openaiApiKey).toBe('sk-openai-from-yaml');
|
|
||||||
});
|
it('should load config with API keys from YAML', () => {
|
||||||
|
const yaml = [
|
||||||
it('should load config without API keys', () => {
|
'language: en',
|
||||||
const yaml = [
|
'default_piece: default',
|
||||||
'language: en',
|
'log_level: info',
|
||||||
'default_piece: default',
|
'provider: claude',
|
||||||
'log_level: info',
|
'anthropic_api_key: sk-ant-from-yaml',
|
||||||
'provider: claude',
|
'openai_api_key: sk-openai-from-yaml',
|
||||||
].join('\n');
|
].join('\n');
|
||||||
writeFileSync(configPath, yaml, 'utf-8');
|
writeFileSync(configPath, yaml, 'utf-8');
|
||||||
|
|
||||||
const config = loadGlobalConfig();
|
const config = loadGlobalConfig();
|
||||||
expect(config.anthropicApiKey).toBeUndefined();
|
expect(config.anthropicApiKey).toBe('sk-ant-from-yaml');
|
||||||
expect(config.openaiApiKey).toBeUndefined();
|
expect(config.openaiApiKey).toBe('sk-openai-from-yaml');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should save and reload config with API keys', () => {
|
it('should load config without API keys', () => {
|
||||||
// Write initial config
|
const yaml = [
|
||||||
const yaml = [
|
'language: en',
|
||||||
'language: en',
|
'default_piece: default',
|
||||||
'default_piece: default',
|
'log_level: info',
|
||||||
'log_level: info',
|
'provider: claude',
|
||||||
'provider: claude',
|
].join('\n');
|
||||||
].join('\n');
|
writeFileSync(configPath, yaml, 'utf-8');
|
||||||
writeFileSync(configPath, yaml, 'utf-8');
|
|
||||||
|
const config = loadGlobalConfig();
|
||||||
const config = loadGlobalConfig();
|
expect(config.anthropicApiKey).toBeUndefined();
|
||||||
config.anthropicApiKey = 'sk-ant-saved';
|
expect(config.openaiApiKey).toBeUndefined();
|
||||||
config.openaiApiKey = 'sk-openai-saved';
|
});
|
||||||
saveGlobalConfig(config);
|
|
||||||
|
it('should save and reload config with API keys', () => {
|
||||||
const reloaded = loadGlobalConfig();
|
// Write initial config
|
||||||
expect(reloaded.anthropicApiKey).toBe('sk-ant-saved');
|
const yaml = [
|
||||||
expect(reloaded.openaiApiKey).toBe('sk-openai-saved');
|
'language: en',
|
||||||
});
|
'default_piece: default',
|
||||||
|
'log_level: info',
|
||||||
it('should not persist API keys when not set', () => {
|
'provider: claude',
|
||||||
const yaml = [
|
].join('\n');
|
||||||
'language: en',
|
writeFileSync(configPath, yaml, 'utf-8');
|
||||||
'default_piece: default',
|
|
||||||
'log_level: info',
|
const config = loadGlobalConfig();
|
||||||
'provider: claude',
|
config.anthropicApiKey = 'sk-ant-saved';
|
||||||
].join('\n');
|
config.openaiApiKey = 'sk-openai-saved';
|
||||||
writeFileSync(configPath, yaml, 'utf-8');
|
saveGlobalConfig(config);
|
||||||
|
|
||||||
const config = loadGlobalConfig();
|
const reloaded = loadGlobalConfig();
|
||||||
saveGlobalConfig(config);
|
expect(reloaded.anthropicApiKey).toBe('sk-ant-saved');
|
||||||
|
expect(reloaded.openaiApiKey).toBe('sk-openai-saved');
|
||||||
const content = readFileSync(configPath, 'utf-8');
|
});
|
||||||
expect(content).not.toContain('anthropic_api_key');
|
|
||||||
expect(content).not.toContain('openai_api_key');
|
it('should not persist API keys when not set', () => {
|
||||||
});
|
const yaml = [
|
||||||
});
|
'language: en',
|
||||||
|
'default_piece: default',
|
||||||
describe('resolveAnthropicApiKey', () => {
|
'log_level: info',
|
||||||
const originalEnv = process.env['TAKT_ANTHROPIC_API_KEY'];
|
'provider: claude',
|
||||||
|
].join('\n');
|
||||||
beforeEach(() => {
|
writeFileSync(configPath, yaml, 'utf-8');
|
||||||
invalidateGlobalConfigCache();
|
|
||||||
mkdirSync(taktDir, { recursive: true });
|
const config = loadGlobalConfig();
|
||||||
});
|
saveGlobalConfig(config);
|
||||||
|
|
||||||
afterEach(() => {
|
const content = readFileSync(configPath, 'utf-8');
|
||||||
if (originalEnv !== undefined) {
|
expect(content).not.toContain('anthropic_api_key');
|
||||||
process.env['TAKT_ANTHROPIC_API_KEY'] = originalEnv;
|
expect(content).not.toContain('openai_api_key');
|
||||||
} else {
|
});
|
||||||
delete process.env['TAKT_ANTHROPIC_API_KEY'];
|
});
|
||||||
}
|
|
||||||
rmSync(testDir, { recursive: true, force: true });
|
describe('resolveAnthropicApiKey', () => {
|
||||||
});
|
const originalEnv = process.env['TAKT_ANTHROPIC_API_KEY'];
|
||||||
|
|
||||||
it('should return env var when set', () => {
|
beforeEach(() => {
|
||||||
process.env['TAKT_ANTHROPIC_API_KEY'] = 'sk-ant-from-env';
|
invalidateGlobalConfigCache();
|
||||||
const yaml = [
|
mkdirSync(taktDir, { recursive: true });
|
||||||
'language: en',
|
});
|
||||||
'default_piece: default',
|
|
||||||
'log_level: info',
|
afterEach(() => {
|
||||||
'provider: claude',
|
if (originalEnv !== undefined) {
|
||||||
'anthropic_api_key: sk-ant-from-yaml',
|
process.env['TAKT_ANTHROPIC_API_KEY'] = originalEnv;
|
||||||
].join('\n');
|
} else {
|
||||||
writeFileSync(configPath, yaml, 'utf-8');
|
delete process.env['TAKT_ANTHROPIC_API_KEY'];
|
||||||
|
}
|
||||||
const key = resolveAnthropicApiKey();
|
rmSync(testDir, { recursive: true, force: true });
|
||||||
expect(key).toBe('sk-ant-from-env');
|
});
|
||||||
});
|
|
||||||
|
it('should return env var when set', () => {
|
||||||
it('should fall back to config when env var is not set', () => {
|
process.env['TAKT_ANTHROPIC_API_KEY'] = 'sk-ant-from-env';
|
||||||
delete process.env['TAKT_ANTHROPIC_API_KEY'];
|
const yaml = [
|
||||||
const yaml = [
|
'language: en',
|
||||||
'language: en',
|
'default_piece: default',
|
||||||
'default_piece: default',
|
'log_level: info',
|
||||||
'log_level: info',
|
'provider: claude',
|
||||||
'provider: claude',
|
'anthropic_api_key: sk-ant-from-yaml',
|
||||||
'anthropic_api_key: sk-ant-from-yaml',
|
].join('\n');
|
||||||
].join('\n');
|
writeFileSync(configPath, yaml, 'utf-8');
|
||||||
writeFileSync(configPath, yaml, 'utf-8');
|
|
||||||
|
const key = resolveAnthropicApiKey();
|
||||||
const key = resolveAnthropicApiKey();
|
expect(key).toBe('sk-ant-from-env');
|
||||||
expect(key).toBe('sk-ant-from-yaml');
|
});
|
||||||
});
|
|
||||||
|
it('should fall back to config when env var is not set', () => {
|
||||||
it('should return undefined when neither env var nor config is set', () => {
|
delete process.env['TAKT_ANTHROPIC_API_KEY'];
|
||||||
delete process.env['TAKT_ANTHROPIC_API_KEY'];
|
const yaml = [
|
||||||
const yaml = [
|
'language: en',
|
||||||
'language: en',
|
'default_piece: default',
|
||||||
'default_piece: default',
|
'log_level: info',
|
||||||
'log_level: info',
|
'provider: claude',
|
||||||
'provider: claude',
|
'anthropic_api_key: sk-ant-from-yaml',
|
||||||
].join('\n');
|
].join('\n');
|
||||||
writeFileSync(configPath, yaml, 'utf-8');
|
writeFileSync(configPath, yaml, 'utf-8');
|
||||||
|
|
||||||
const key = resolveAnthropicApiKey();
|
const key = resolveAnthropicApiKey();
|
||||||
expect(key).toBeUndefined();
|
expect(key).toBe('sk-ant-from-yaml');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return undefined when config file does not exist', () => {
|
it('should return undefined when neither env var nor config is set', () => {
|
||||||
delete process.env['TAKT_ANTHROPIC_API_KEY'];
|
delete process.env['TAKT_ANTHROPIC_API_KEY'];
|
||||||
// No config file created
|
const yaml = [
|
||||||
rmSync(testDir, { recursive: true, force: true });
|
'language: en',
|
||||||
|
'default_piece: default',
|
||||||
const key = resolveAnthropicApiKey();
|
'log_level: info',
|
||||||
expect(key).toBeUndefined();
|
'provider: claude',
|
||||||
});
|
].join('\n');
|
||||||
});
|
writeFileSync(configPath, yaml, 'utf-8');
|
||||||
|
|
||||||
describe('resolveOpenaiApiKey', () => {
|
const key = resolveAnthropicApiKey();
|
||||||
const originalEnv = process.env['TAKT_OPENAI_API_KEY'];
|
expect(key).toBeUndefined();
|
||||||
|
});
|
||||||
beforeEach(() => {
|
|
||||||
invalidateGlobalConfigCache();
|
it('should return undefined when config file does not exist', () => {
|
||||||
mkdirSync(taktDir, { recursive: true });
|
delete process.env['TAKT_ANTHROPIC_API_KEY'];
|
||||||
});
|
// No config file created
|
||||||
|
rmSync(testDir, { recursive: true, force: true });
|
||||||
afterEach(() => {
|
|
||||||
if (originalEnv !== undefined) {
|
const key = resolveAnthropicApiKey();
|
||||||
process.env['TAKT_OPENAI_API_KEY'] = originalEnv;
|
expect(key).toBeUndefined();
|
||||||
} else {
|
});
|
||||||
delete process.env['TAKT_OPENAI_API_KEY'];
|
});
|
||||||
}
|
|
||||||
rmSync(testDir, { recursive: true, force: true });
|
describe('resolveOpenaiApiKey', () => {
|
||||||
});
|
const originalEnv = process.env['TAKT_OPENAI_API_KEY'];
|
||||||
|
|
||||||
it('should return env var when set', () => {
|
beforeEach(() => {
|
||||||
process.env['TAKT_OPENAI_API_KEY'] = 'sk-openai-from-env';
|
invalidateGlobalConfigCache();
|
||||||
const yaml = [
|
mkdirSync(taktDir, { recursive: true });
|
||||||
'language: en',
|
});
|
||||||
'default_piece: default',
|
|
||||||
'log_level: info',
|
afterEach(() => {
|
||||||
'provider: claude',
|
if (originalEnv !== undefined) {
|
||||||
'openai_api_key: sk-openai-from-yaml',
|
process.env['TAKT_OPENAI_API_KEY'] = originalEnv;
|
||||||
].join('\n');
|
} else {
|
||||||
writeFileSync(configPath, yaml, 'utf-8');
|
delete process.env['TAKT_OPENAI_API_KEY'];
|
||||||
|
}
|
||||||
const key = resolveOpenaiApiKey();
|
rmSync(testDir, { recursive: true, force: true });
|
||||||
expect(key).toBe('sk-openai-from-env');
|
});
|
||||||
});
|
|
||||||
|
it('should return env var when set', () => {
|
||||||
it('should fall back to config when env var is not set', () => {
|
process.env['TAKT_OPENAI_API_KEY'] = 'sk-openai-from-env';
|
||||||
delete process.env['TAKT_OPENAI_API_KEY'];
|
const yaml = [
|
||||||
const yaml = [
|
'language: en',
|
||||||
'language: en',
|
'default_piece: default',
|
||||||
'default_piece: default',
|
'log_level: info',
|
||||||
'log_level: info',
|
'provider: claude',
|
||||||
'provider: claude',
|
'openai_api_key: sk-openai-from-yaml',
|
||||||
'openai_api_key: sk-openai-from-yaml',
|
].join('\n');
|
||||||
].join('\n');
|
writeFileSync(configPath, yaml, 'utf-8');
|
||||||
writeFileSync(configPath, yaml, 'utf-8');
|
|
||||||
|
const key = resolveOpenaiApiKey();
|
||||||
const key = resolveOpenaiApiKey();
|
expect(key).toBe('sk-openai-from-env');
|
||||||
expect(key).toBe('sk-openai-from-yaml');
|
});
|
||||||
});
|
|
||||||
|
it('should fall back to config when env var is not set', () => {
|
||||||
it('should return undefined when neither env var nor config is set', () => {
|
delete process.env['TAKT_OPENAI_API_KEY'];
|
||||||
delete process.env['TAKT_OPENAI_API_KEY'];
|
const yaml = [
|
||||||
const yaml = [
|
'language: en',
|
||||||
'language: en',
|
'default_piece: default',
|
||||||
'default_piece: default',
|
'log_level: info',
|
||||||
'log_level: info',
|
'provider: claude',
|
||||||
'provider: claude',
|
'openai_api_key: sk-openai-from-yaml',
|
||||||
].join('\n');
|
].join('\n');
|
||||||
writeFileSync(configPath, yaml, 'utf-8');
|
writeFileSync(configPath, yaml, 'utf-8');
|
||||||
|
|
||||||
const key = resolveOpenaiApiKey();
|
const key = resolveOpenaiApiKey();
|
||||||
expect(key).toBeUndefined();
|
expect(key).toBe('sk-openai-from-yaml');
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
it('should return undefined when neither env var nor config is set', () => {
|
||||||
describe('resolveOpencodeApiKey', () => {
|
delete process.env['TAKT_OPENAI_API_KEY'];
|
||||||
const originalEnv = process.env['TAKT_OPENCODE_API_KEY'];
|
const yaml = [
|
||||||
|
'language: en',
|
||||||
beforeEach(() => {
|
'default_piece: default',
|
||||||
invalidateGlobalConfigCache();
|
'log_level: info',
|
||||||
mkdirSync(taktDir, { recursive: true });
|
'provider: claude',
|
||||||
});
|
].join('\n');
|
||||||
|
writeFileSync(configPath, yaml, 'utf-8');
|
||||||
afterEach(() => {
|
|
||||||
if (originalEnv !== undefined) {
|
const key = resolveOpenaiApiKey();
|
||||||
process.env['TAKT_OPENCODE_API_KEY'] = originalEnv;
|
expect(key).toBeUndefined();
|
||||||
} else {
|
});
|
||||||
delete process.env['TAKT_OPENCODE_API_KEY'];
|
});
|
||||||
}
|
|
||||||
rmSync(testDir, { recursive: true, force: true });
|
describe('resolveCodexCliPath', () => {
|
||||||
});
|
const originalEnv = process.env['TAKT_CODEX_CLI_PATH'];
|
||||||
|
|
||||||
it('should return env var when set', () => {
|
beforeEach(() => {
|
||||||
process.env['TAKT_OPENCODE_API_KEY'] = 'sk-opencode-from-env';
|
invalidateGlobalConfigCache();
|
||||||
const yaml = [
|
mkdirSync(taktDir, { recursive: true });
|
||||||
'language: en',
|
});
|
||||||
'default_piece: default',
|
|
||||||
'log_level: info',
|
afterEach(() => {
|
||||||
'provider: claude',
|
if (originalEnv !== undefined) {
|
||||||
'opencode_api_key: sk-opencode-from-yaml',
|
process.env['TAKT_CODEX_CLI_PATH'] = originalEnv;
|
||||||
].join('\n');
|
} else {
|
||||||
writeFileSync(configPath, yaml, 'utf-8');
|
delete process.env['TAKT_CODEX_CLI_PATH'];
|
||||||
|
}
|
||||||
const key = resolveOpencodeApiKey();
|
rmSync(testDir, { recursive: true, force: true });
|
||||||
expect(key).toBe('sk-opencode-from-env');
|
});
|
||||||
});
|
|
||||||
|
it('should return env var path when set', () => {
|
||||||
it('should fall back to config when env var is not set', () => {
|
const envCodexPath = createExecutableFile('env-codex');
|
||||||
delete process.env['TAKT_OPENCODE_API_KEY'];
|
const configCodexPath = createExecutableFile('config-codex');
|
||||||
const yaml = [
|
process.env['TAKT_CODEX_CLI_PATH'] = envCodexPath;
|
||||||
'language: en',
|
const yaml = [
|
||||||
'default_piece: default',
|
'language: en',
|
||||||
'log_level: info',
|
'default_piece: default',
|
||||||
'provider: claude',
|
'log_level: info',
|
||||||
'opencode_api_key: sk-opencode-from-yaml',
|
'provider: codex',
|
||||||
].join('\n');
|
`codex_cli_path: ${configCodexPath}`,
|
||||||
writeFileSync(configPath, yaml, 'utf-8');
|
].join('\n');
|
||||||
|
writeFileSync(configPath, yaml, 'utf-8');
|
||||||
const key = resolveOpencodeApiKey();
|
|
||||||
expect(key).toBe('sk-opencode-from-yaml');
|
const path = resolveCodexCliPath();
|
||||||
});
|
expect(path).toBe(envCodexPath);
|
||||||
|
});
|
||||||
it('should return undefined when neither env var nor config is set', () => {
|
|
||||||
delete process.env['TAKT_OPENCODE_API_KEY'];
|
it('should fall back to config path when env var is not set', () => {
|
||||||
const yaml = [
|
delete process.env['TAKT_CODEX_CLI_PATH'];
|
||||||
'language: en',
|
const configCodexPath = createExecutableFile('config-codex');
|
||||||
'default_piece: default',
|
const yaml = [
|
||||||
'log_level: info',
|
'language: en',
|
||||||
'provider: claude',
|
'default_piece: default',
|
||||||
].join('\n');
|
'log_level: info',
|
||||||
writeFileSync(configPath, yaml, 'utf-8');
|
'provider: codex',
|
||||||
|
`codex_cli_path: ${configCodexPath}`,
|
||||||
const key = resolveOpencodeApiKey();
|
].join('\n');
|
||||||
expect(key).toBeUndefined();
|
writeFileSync(configPath, yaml, 'utf-8');
|
||||||
});
|
|
||||||
});
|
const path = resolveCodexCliPath();
|
||||||
|
expect(path).toBe(configCodexPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return undefined when neither env var nor config is set', () => {
|
||||||
|
delete process.env['TAKT_CODEX_CLI_PATH'];
|
||||||
|
const yaml = [
|
||||||
|
'language: en',
|
||||||
|
'default_piece: default',
|
||||||
|
'log_level: info',
|
||||||
|
'provider: codex',
|
||||||
|
].join('\n');
|
||||||
|
writeFileSync(configPath, yaml, 'utf-8');
|
||||||
|
|
||||||
|
const path = resolveCodexCliPath();
|
||||||
|
expect(path).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw when env path is empty', () => {
|
||||||
|
process.env['TAKT_CODEX_CLI_PATH'] = '';
|
||||||
|
expect(() => resolveCodexCliPath()).toThrow(/must not be empty/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw when env path does not exist', () => {
|
||||||
|
process.env['TAKT_CODEX_CLI_PATH'] = join(testDir, 'missing-codex');
|
||||||
|
expect(() => resolveCodexCliPath()).toThrow(/does not exist/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw when env path points to a directory', () => {
|
||||||
|
const dirPath = join(testDir, 'codex-dir');
|
||||||
|
mkdirSync(dirPath, { recursive: true });
|
||||||
|
process.env['TAKT_CODEX_CLI_PATH'] = dirPath;
|
||||||
|
expect(() => resolveCodexCliPath()).toThrow(/executable file/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw when env path points to a non-executable file', () => {
|
||||||
|
process.env['TAKT_CODEX_CLI_PATH'] = createNonExecutableFile('non-executable-codex');
|
||||||
|
expect(() => resolveCodexCliPath()).toThrow(/not executable/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw when env path is relative', () => {
|
||||||
|
process.env['TAKT_CODEX_CLI_PATH'] = 'bin/codex';
|
||||||
|
expect(() => resolveCodexCliPath()).toThrow(/absolute path/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw when env path contains control characters', () => {
|
||||||
|
process.env['TAKT_CODEX_CLI_PATH'] = '/tmp/codex\nbad';
|
||||||
|
expect(() => resolveCodexCliPath()).toThrow(/control characters/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw when config path is invalid', () => {
|
||||||
|
delete process.env['TAKT_CODEX_CLI_PATH'];
|
||||||
|
const yaml = [
|
||||||
|
'language: en',
|
||||||
|
'default_piece: default',
|
||||||
|
'log_level: info',
|
||||||
|
'provider: codex',
|
||||||
|
`codex_cli_path: ${join(testDir, 'missing-codex-from-config')}`,
|
||||||
|
].join('\n');
|
||||||
|
writeFileSync(configPath, yaml, 'utf-8');
|
||||||
|
|
||||||
|
expect(() => resolveCodexCliPath()).toThrow(/does not exist/i);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('resolveOpencodeApiKey', () => {
|
||||||
|
const originalEnv = process.env['TAKT_OPENCODE_API_KEY'];
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
invalidateGlobalConfigCache();
|
||||||
|
mkdirSync(taktDir, { recursive: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (originalEnv !== undefined) {
|
||||||
|
process.env['TAKT_OPENCODE_API_KEY'] = originalEnv;
|
||||||
|
} else {
|
||||||
|
delete process.env['TAKT_OPENCODE_API_KEY'];
|
||||||
|
}
|
||||||
|
rmSync(testDir, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return env var when set', () => {
|
||||||
|
process.env['TAKT_OPENCODE_API_KEY'] = 'sk-opencode-from-env';
|
||||||
|
const yaml = [
|
||||||
|
'language: en',
|
||||||
|
'default_piece: default',
|
||||||
|
'log_level: info',
|
||||||
|
'provider: claude',
|
||||||
|
'opencode_api_key: sk-opencode-from-yaml',
|
||||||
|
].join('\n');
|
||||||
|
writeFileSync(configPath, yaml, 'utf-8');
|
||||||
|
|
||||||
|
const key = resolveOpencodeApiKey();
|
||||||
|
expect(key).toBe('sk-opencode-from-env');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fall back to config when env var is not set', () => {
|
||||||
|
delete process.env['TAKT_OPENCODE_API_KEY'];
|
||||||
|
const yaml = [
|
||||||
|
'language: en',
|
||||||
|
'default_piece: default',
|
||||||
|
'log_level: info',
|
||||||
|
'provider: claude',
|
||||||
|
'opencode_api_key: sk-opencode-from-yaml',
|
||||||
|
].join('\n');
|
||||||
|
writeFileSync(configPath, yaml, 'utf-8');
|
||||||
|
|
||||||
|
const key = resolveOpencodeApiKey();
|
||||||
|
expect(key).toBe('sk-opencode-from-yaml');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return undefined when neither env var nor config is set', () => {
|
||||||
|
delete process.env['TAKT_OPENCODE_API_KEY'];
|
||||||
|
const yaml = [
|
||||||
|
'language: en',
|
||||||
|
'default_piece: default',
|
||||||
|
'log_level: info',
|
||||||
|
'provider: claude',
|
||||||
|
].join('\n');
|
||||||
|
writeFileSync(configPath, yaml, 'utf-8');
|
||||||
|
|
||||||
|
const key = resolveOpencodeApiKey();
|
||||||
|
expect(key).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -56,6 +56,7 @@ vi.mock('../infra/opencode/index.js', () => ({
|
|||||||
vi.mock('../infra/config/index.js', () => ({
|
vi.mock('../infra/config/index.js', () => ({
|
||||||
resolveAnthropicApiKey: vi.fn(() => undefined),
|
resolveAnthropicApiKey: vi.fn(() => undefined),
|
||||||
resolveOpenaiApiKey: vi.fn(() => undefined),
|
resolveOpenaiApiKey: vi.fn(() => undefined),
|
||||||
|
resolveCodexCliPath: vi.fn(() => '/opt/codex/bin/codex'),
|
||||||
resolveOpencodeApiKey: vi.fn(() => undefined),
|
resolveOpencodeApiKey: vi.fn(() => undefined),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -148,6 +149,7 @@ describe('CodexProvider — structured output', () => {
|
|||||||
|
|
||||||
const opts = mockCallCodex.mock.calls[0]?.[2];
|
const opts = mockCallCodex.mock.calls[0]?.[2];
|
||||||
expect(opts).toHaveProperty('outputSchema', SCHEMA);
|
expect(opts).toHaveProperty('outputSchema', SCHEMA);
|
||||||
|
expect(opts).toHaveProperty('codexPathOverride', '/opt/codex/bin/codex');
|
||||||
expect(result.structuredOutput).toEqual({ step: 2 });
|
expect(result.structuredOutput).toEqual({ step: 2 });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -77,6 +77,8 @@ export interface GlobalConfig {
|
|||||||
anthropicApiKey?: string;
|
anthropicApiKey?: string;
|
||||||
/** OpenAI API key for Codex SDK (overridden by TAKT_OPENAI_API_KEY env var) */
|
/** OpenAI API key for Codex SDK (overridden by TAKT_OPENAI_API_KEY env var) */
|
||||||
openaiApiKey?: string;
|
openaiApiKey?: string;
|
||||||
|
/** External Codex CLI path for Codex SDK override (overridden by TAKT_CODEX_CLI_PATH env var) */
|
||||||
|
codexCliPath?: string;
|
||||||
/** OpenCode API key for OpenCode SDK (overridden by TAKT_OPENCODE_API_KEY env var) */
|
/** OpenCode API key for OpenCode SDK (overridden by TAKT_OPENCODE_API_KEY env var) */
|
||||||
opencodeApiKey?: string;
|
opencodeApiKey?: string;
|
||||||
/** Pipeline execution settings */
|
/** Pipeline execution settings */
|
||||||
|
|||||||
@ -429,6 +429,8 @@ export const GlobalConfigSchema = z.object({
|
|||||||
anthropic_api_key: z.string().optional(),
|
anthropic_api_key: z.string().optional(),
|
||||||
/** OpenAI API key for Codex SDK (overridden by TAKT_OPENAI_API_KEY env var) */
|
/** OpenAI API key for Codex SDK (overridden by TAKT_OPENAI_API_KEY env var) */
|
||||||
openai_api_key: z.string().optional(),
|
openai_api_key: z.string().optional(),
|
||||||
|
/** External Codex CLI path for Codex SDK override (overridden by TAKT_CODEX_CLI_PATH env var) */
|
||||||
|
codex_cli_path: z.string().optional(),
|
||||||
/** OpenCode API key for OpenCode SDK (overridden by TAKT_OPENCODE_API_KEY env var) */
|
/** OpenCode API key for OpenCode SDK (overridden by TAKT_OPENCODE_API_KEY env var) */
|
||||||
opencode_api_key: z.string().optional(),
|
opencode_api_key: z.string().optional(),
|
||||||
/** Pipeline execution settings */
|
/** Pipeline execution settings */
|
||||||
|
|||||||
@ -104,7 +104,11 @@ export class CodexClient {
|
|||||||
: prompt;
|
: prompt;
|
||||||
|
|
||||||
for (let attempt = 1; attempt <= CODEX_RETRY_MAX_ATTEMPTS; attempt++) {
|
for (let attempt = 1; attempt <= CODEX_RETRY_MAX_ATTEMPTS; attempt++) {
|
||||||
const codex = new Codex(options.openaiApiKey ? { apiKey: options.openaiApiKey } : undefined);
|
const codexClientOptions = {
|
||||||
|
...(options.openaiApiKey ? { apiKey: options.openaiApiKey } : {}),
|
||||||
|
...(options.codexPathOverride ? { codexPathOverride: options.codexPathOverride } : {}),
|
||||||
|
};
|
||||||
|
const codex = new Codex(Object.keys(codexClientOptions).length > 0 ? codexClientOptions : undefined);
|
||||||
const thread = threadId
|
const thread = threadId
|
||||||
? await codex.resumeThread(threadId, threadOptions)
|
? await codex.resumeThread(threadId, threadOptions)
|
||||||
: await codex.startThread(threadOptions);
|
: await codex.startThread(threadOptions);
|
||||||
|
|||||||
@ -33,6 +33,8 @@ export interface CodexCallOptions {
|
|||||||
onStream?: StreamCallback;
|
onStream?: StreamCallback;
|
||||||
/** OpenAI API key (bypasses CLI auth) */
|
/** OpenAI API key (bypasses CLI auth) */
|
||||||
openaiApiKey?: string;
|
openaiApiKey?: string;
|
||||||
|
/** Override path to external Codex CLI binary (bypasses SDK vendored binary) */
|
||||||
|
codexPathOverride?: string;
|
||||||
/** JSON Schema for structured output */
|
/** JSON Schema for structured output */
|
||||||
outputSchema?: Record<string, unknown>;
|
outputSchema?: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,7 +5,8 @@
|
|||||||
* GlobalConfigManager encapsulates the config cache as a singleton.
|
* GlobalConfigManager encapsulates the config cache as a singleton.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { readFileSync, existsSync, writeFileSync } from 'node:fs';
|
import { readFileSync, existsSync, writeFileSync, statSync, accessSync, constants } from 'node:fs';
|
||||||
|
import { isAbsolute } from 'node:path';
|
||||||
import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
|
import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
|
||||||
import { GlobalConfigSchema } from '../../../core/models/index.js';
|
import { GlobalConfigSchema } from '../../../core/models/index.js';
|
||||||
import type { GlobalConfig, DebugConfig, Language } from '../../../core/models/index.js';
|
import type { GlobalConfig, DebugConfig, Language } from '../../../core/models/index.js';
|
||||||
@ -18,6 +19,42 @@ import { parseProviderModel } from '../../../shared/utils/providerModel.js';
|
|||||||
/** Claude-specific model aliases that are not valid for other providers */
|
/** Claude-specific model aliases that are not valid for other providers */
|
||||||
const CLAUDE_MODEL_ALIASES = new Set(['opus', 'sonnet', 'haiku']);
|
const CLAUDE_MODEL_ALIASES = new Set(['opus', 'sonnet', 'haiku']);
|
||||||
|
|
||||||
|
function hasControlCharacters(value: string): boolean {
|
||||||
|
for (let index = 0; index < value.length; index++) {
|
||||||
|
const code = value.charCodeAt(index);
|
||||||
|
if (code < 32 || code === 127) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateCodexCliPath(pathValue: string, sourceName: 'TAKT_CODEX_CLI_PATH' | 'codex_cli_path'): string {
|
||||||
|
const trimmed = pathValue.trim();
|
||||||
|
if (trimmed.length === 0) {
|
||||||
|
throw new Error(`Configuration error: ${sourceName} must not be empty.`);
|
||||||
|
}
|
||||||
|
if (hasControlCharacters(trimmed)) {
|
||||||
|
throw new Error(`Configuration error: ${sourceName} contains control characters.`);
|
||||||
|
}
|
||||||
|
if (!isAbsolute(trimmed)) {
|
||||||
|
throw new Error(`Configuration error: ${sourceName} must be an absolute path: ${trimmed}`);
|
||||||
|
}
|
||||||
|
if (!existsSync(trimmed)) {
|
||||||
|
throw new Error(`Configuration error: ${sourceName} path does not exist: ${trimmed}`);
|
||||||
|
}
|
||||||
|
const stats = statSync(trimmed);
|
||||||
|
if (!stats.isFile()) {
|
||||||
|
throw new Error(`Configuration error: ${sourceName} must point to an executable file: ${trimmed}`);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
accessSync(trimmed, constants.X_OK);
|
||||||
|
} catch {
|
||||||
|
throw new Error(`Configuration error: ${sourceName} file is not executable: ${trimmed}`);
|
||||||
|
}
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
/** Validate that provider and model are compatible */
|
/** Validate that provider and model are compatible */
|
||||||
function validateProviderModelCompatibility(provider: string | undefined, model: string | undefined): void {
|
function validateProviderModelCompatibility(provider: string | undefined, model: string | undefined): void {
|
||||||
if (!provider) return;
|
if (!provider) return;
|
||||||
@ -144,6 +181,7 @@ export class GlobalConfigManager {
|
|||||||
enableBuiltinPieces: parsed.enable_builtin_pieces,
|
enableBuiltinPieces: parsed.enable_builtin_pieces,
|
||||||
anthropicApiKey: parsed.anthropic_api_key,
|
anthropicApiKey: parsed.anthropic_api_key,
|
||||||
openaiApiKey: parsed.openai_api_key,
|
openaiApiKey: parsed.openai_api_key,
|
||||||
|
codexCliPath: parsed.codex_cli_path,
|
||||||
opencodeApiKey: parsed.opencode_api_key,
|
opencodeApiKey: parsed.opencode_api_key,
|
||||||
pipeline: parsed.pipeline ? {
|
pipeline: parsed.pipeline ? {
|
||||||
defaultBranchPrefix: parsed.pipeline.default_branch_prefix,
|
defaultBranchPrefix: parsed.pipeline.default_branch_prefix,
|
||||||
@ -219,6 +257,9 @@ export class GlobalConfigManager {
|
|||||||
if (config.openaiApiKey) {
|
if (config.openaiApiKey) {
|
||||||
raw.openai_api_key = config.openaiApiKey;
|
raw.openai_api_key = config.openaiApiKey;
|
||||||
}
|
}
|
||||||
|
if (config.codexCliPath) {
|
||||||
|
raw.codex_cli_path = config.codexCliPath;
|
||||||
|
}
|
||||||
if (config.opencodeApiKey) {
|
if (config.opencodeApiKey) {
|
||||||
raw.opencode_api_key = config.opencodeApiKey;
|
raw.opencode_api_key = config.opencodeApiKey;
|
||||||
}
|
}
|
||||||
@ -379,6 +420,28 @@ export function resolveOpenaiApiKey(): string | undefined {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve the Codex CLI path override.
|
||||||
|
* Priority: TAKT_CODEX_CLI_PATH env var > config.yaml > undefined (SDK vendored binary fallback)
|
||||||
|
*/
|
||||||
|
export function resolveCodexCliPath(): string | undefined {
|
||||||
|
const envPath = process.env['TAKT_CODEX_CLI_PATH'];
|
||||||
|
if (envPath !== undefined) {
|
||||||
|
return validateCodexCliPath(envPath, 'TAKT_CODEX_CLI_PATH');
|
||||||
|
}
|
||||||
|
|
||||||
|
let config: GlobalConfig;
|
||||||
|
try {
|
||||||
|
config = loadGlobalConfig();
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (config.codexCliPath === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return validateCodexCliPath(config.codexCliPath, 'codex_cli_path');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve the OpenCode API key.
|
* Resolve the OpenCode API key.
|
||||||
* Priority: TAKT_OPENCODE_API_KEY env var > config.yaml > undefined
|
* Priority: TAKT_OPENCODE_API_KEY env var > config.yaml > undefined
|
||||||
|
|||||||
@ -14,6 +14,7 @@ export {
|
|||||||
setProvider,
|
setProvider,
|
||||||
resolveAnthropicApiKey,
|
resolveAnthropicApiKey,
|
||||||
resolveOpenaiApiKey,
|
resolveOpenaiApiKey,
|
||||||
|
resolveCodexCliPath,
|
||||||
resolveOpencodeApiKey,
|
resolveOpencodeApiKey,
|
||||||
loadProjectDebugConfig,
|
loadProjectDebugConfig,
|
||||||
getEffectiveDebugConfig,
|
getEffectiveDebugConfig,
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
import { execFileSync } from 'node:child_process';
|
import { execFileSync } from 'node:child_process';
|
||||||
import { callCodex, callCodexCustom, type CodexCallOptions } from '../codex/index.js';
|
import { callCodex, callCodexCustom, type CodexCallOptions } from '../codex/index.js';
|
||||||
import { resolveOpenaiApiKey } from '../config/index.js';
|
import { resolveOpenaiApiKey, resolveCodexCliPath } from '../config/index.js';
|
||||||
import type { AgentResponse } from '../../core/models/index.js';
|
import type { AgentResponse } from '../../core/models/index.js';
|
||||||
import type { AgentSetup, Provider, ProviderAgent, ProviderCallOptions } from './types.js';
|
import type { AgentSetup, Provider, ProviderAgent, ProviderCallOptions } from './types.js';
|
||||||
|
|
||||||
@ -34,6 +34,7 @@ function toCodexOptions(options: ProviderCallOptions): CodexCallOptions {
|
|||||||
networkAccess: options.providerOptions?.codex?.networkAccess,
|
networkAccess: options.providerOptions?.codex?.networkAccess,
|
||||||
onStream: options.onStream,
|
onStream: options.onStream,
|
||||||
openaiApiKey: options.openaiApiKey ?? resolveOpenaiApiKey(),
|
openaiApiKey: options.openaiApiKey ?? resolveOpenaiApiKey(),
|
||||||
|
codexPathOverride: resolveCodexCliPath(),
|
||||||
outputSchema: options.outputSchema,
|
outputSchema: options.outputSchema,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user