feat: プロジェクト単位のCLIパス設定(Claude/Cursor/Codex) (#413)
* feat: プロジェクト単位のCLIパス設定を支援するconfig層を追加 validateCliPath汎用関数、Global/Project設定スキーマ拡張、 env override、3プロバイダ向けresolve関数(env→project→global→undefined)を追加。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: Claude/Cursor/CodexプロバイダにCLIパス解決を統合 各プロバイダのtoXxxOptions()でproject configを読み込み、 resolveXxxCliPath()経由でCLIパスを解決してSDKに渡す。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test: per-project CLIパス機能のテストを追加 validateCliPath, resolveClaudeCliPath, resolveCursorCliPath, resolveCodexCliPath(project config層)のユニットテスト、 および既存プロバイダテストのモック更新。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
52c5e29000
commit
b8b64f858b
@ -18,6 +18,8 @@ vi.mock('../infra/claude/client.js', () => ({
|
|||||||
|
|
||||||
vi.mock('../infra/config/index.js', () => ({
|
vi.mock('../infra/config/index.js', () => ({
|
||||||
resolveAnthropicApiKey: mockResolveAnthropicApiKey,
|
resolveAnthropicApiKey: mockResolveAnthropicApiKey,
|
||||||
|
resolveClaudeCliPath: vi.fn(() => undefined),
|
||||||
|
loadProjectConfig: vi.fn(() => ({})),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import { ClaudeProvider } from '../infra/providers/claude.js';
|
import { ClaudeProvider } from '../infra/providers/claude.js';
|
||||||
|
|||||||
@ -12,8 +12,14 @@ const {
|
|||||||
mockCallCursorCustom: vi.fn(),
|
mockCallCursorCustom: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const { mockResolveCursorApiKey } = vi.hoisted(() => ({
|
const {
|
||||||
|
mockResolveCursorApiKey,
|
||||||
|
mockResolveCursorCliPath,
|
||||||
|
mockLoadProjectConfig,
|
||||||
|
} = vi.hoisted(() => ({
|
||||||
mockResolveCursorApiKey: vi.fn(() => undefined),
|
mockResolveCursorApiKey: vi.fn(() => undefined),
|
||||||
|
mockResolveCursorCliPath: vi.fn(() => undefined),
|
||||||
|
mockLoadProjectConfig: vi.fn(() => ({})),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../infra/cursor/index.js', () => ({
|
vi.mock('../infra/cursor/index.js', () => ({
|
||||||
@ -23,6 +29,8 @@ vi.mock('../infra/cursor/index.js', () => ({
|
|||||||
|
|
||||||
vi.mock('../infra/config/index.js', () => ({
|
vi.mock('../infra/config/index.js', () => ({
|
||||||
resolveCursorApiKey: mockResolveCursorApiKey,
|
resolveCursorApiKey: mockResolveCursorApiKey,
|
||||||
|
resolveCursorCliPath: mockResolveCursorCliPath,
|
||||||
|
loadProjectConfig: mockLoadProjectConfig,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import { CursorProvider } from '../infra/providers/cursor.js';
|
import { CursorProvider } from '../infra/providers/cursor.js';
|
||||||
@ -41,6 +49,8 @@ describe('CursorProvider', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
mockResolveCursorApiKey.mockReturnValue(undefined);
|
mockResolveCursorApiKey.mockReturnValue(undefined);
|
||||||
|
mockResolveCursorCliPath.mockReturnValue(undefined);
|
||||||
|
mockLoadProjectConfig.mockReturnValue({});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw when claudeAgent is specified', () => {
|
it('should throw when claudeAgent is specified', () => {
|
||||||
@ -129,6 +139,37 @@ describe('CursorProvider', () => {
|
|||||||
expect.objectContaining({ cwd: '/tmp/work' }),
|
expect.objectContaining({ cwd: '/tmp/work' }),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should pass resolved cursorCliPath to callCursor', async () => {
|
||||||
|
mockResolveCursorCliPath.mockReturnValue('/custom/bin/cursor-agent');
|
||||||
|
mockCallCursor.mockResolvedValue(doneResponse('coder'));
|
||||||
|
|
||||||
|
const provider = new CursorProvider();
|
||||||
|
const agent = provider.setup({ name: 'coder' });
|
||||||
|
|
||||||
|
await agent.call('implement', { cwd: '/tmp/work' });
|
||||||
|
|
||||||
|
expect(mockCallCursor).toHaveBeenCalledWith(
|
||||||
|
'coder',
|
||||||
|
'implement',
|
||||||
|
expect.objectContaining({
|
||||||
|
cursorCliPath: '/custom/bin/cursor-agent',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass undefined cursorCliPath when resolver returns undefined', async () => {
|
||||||
|
mockResolveCursorCliPath.mockReturnValue(undefined);
|
||||||
|
mockCallCursor.mockResolvedValue(doneResponse('coder'));
|
||||||
|
|
||||||
|
const provider = new CursorProvider();
|
||||||
|
const agent = provider.setup({ name: 'coder' });
|
||||||
|
|
||||||
|
await agent.call('implement', { cwd: '/tmp/work' });
|
||||||
|
|
||||||
|
const opts = mockCallCursor.mock.calls[0]?.[2];
|
||||||
|
expect(opts.cursorCliPath).toBeUndefined();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('ProviderRegistry with Cursor', () => {
|
describe('ProviderRegistry with Cursor', () => {
|
||||||
|
|||||||
@ -52,8 +52,11 @@ const {
|
|||||||
resolveAnthropicApiKey,
|
resolveAnthropicApiKey,
|
||||||
resolveOpenaiApiKey,
|
resolveOpenaiApiKey,
|
||||||
resolveCodexCliPath,
|
resolveCodexCliPath,
|
||||||
|
resolveClaudeCliPath,
|
||||||
|
resolveCursorCliPath,
|
||||||
resolveOpencodeApiKey,
|
resolveOpencodeApiKey,
|
||||||
resolveCursorApiKey,
|
resolveCursorApiKey,
|
||||||
|
validateCliPath,
|
||||||
invalidateGlobalConfigCache,
|
invalidateGlobalConfigCache,
|
||||||
} = await import('../infra/config/global/globalConfig.js');
|
} = await import('../infra/config/global/globalConfig.js');
|
||||||
|
|
||||||
@ -531,3 +534,311 @@ describe('resolveCursorApiKey', () => {
|
|||||||
expect(key).toBeUndefined();
|
expect(key).toBeUndefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Task 6.1 — validateCliPath unit tests
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
describe('validateCliPath', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mkdirSync(testDir, { recursive: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
rmSync(testDir, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return trimmed path for a valid executable', () => {
|
||||||
|
const exePath = createExecutableFile('valid-cli');
|
||||||
|
const result = validateCliPath(exePath, 'test_cli_path');
|
||||||
|
expect(result).toBe(exePath);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should trim whitespace from the path', () => {
|
||||||
|
const exePath = createExecutableFile('valid-cli');
|
||||||
|
const result = validateCliPath(` ${exePath} `, 'test_cli_path');
|
||||||
|
expect(result).toBe(exePath);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw when path is empty', () => {
|
||||||
|
expect(() => validateCliPath('', 'test_cli_path')).toThrow(/must not be empty/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw when path is only whitespace', () => {
|
||||||
|
expect(() => validateCliPath(' ', 'test_cli_path')).toThrow(/must not be empty/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw when path contains control characters', () => {
|
||||||
|
expect(() => validateCliPath('/tmp/cli\nbad', 'test_cli_path')).toThrow(/control characters/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw when path is relative', () => {
|
||||||
|
expect(() => validateCliPath('bin/cli', 'test_cli_path')).toThrow(/absolute path/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw when path does not exist', () => {
|
||||||
|
expect(() => validateCliPath(join(testDir, 'missing'), 'test_cli_path')).toThrow(/does not exist/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw when path points to a directory', () => {
|
||||||
|
const dirPath = join(testDir, 'a-dir');
|
||||||
|
mkdirSync(dirPath, { recursive: true });
|
||||||
|
expect(() => validateCliPath(dirPath, 'test_cli_path')).toThrow(/executable file/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw when path points to a non-executable file', () => {
|
||||||
|
const filePath = createNonExecutableFile('non-exec');
|
||||||
|
expect(() => validateCliPath(filePath, 'test_cli_path')).toThrow(/not executable/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include source name in error messages', () => {
|
||||||
|
expect(() => validateCliPath('', 'MY_CUSTOM_SOURCE')).toThrow(/MY_CUSTOM_SOURCE/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Task 6.2 — resolveClaudeCliPath / resolveCursorCliPath tests
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
describe('resolveClaudeCliPath', () => {
|
||||||
|
const originalEnv = process.env['TAKT_CLAUDE_CLI_PATH'];
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
invalidateGlobalConfigCache();
|
||||||
|
mkdirSync(taktDir, { recursive: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (originalEnv !== undefined) {
|
||||||
|
process.env['TAKT_CLAUDE_CLI_PATH'] = originalEnv;
|
||||||
|
} else {
|
||||||
|
delete process.env['TAKT_CLAUDE_CLI_PATH'];
|
||||||
|
}
|
||||||
|
rmSync(testDir, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return env var path when set (highest priority)', () => {
|
||||||
|
const envPath = createExecutableFile('env-claude');
|
||||||
|
const configPath2 = createExecutableFile('config-claude');
|
||||||
|
process.env['TAKT_CLAUDE_CLI_PATH'] = envPath;
|
||||||
|
const yaml = [
|
||||||
|
'language: en',
|
||||||
|
'log_level: info',
|
||||||
|
'provider: claude',
|
||||||
|
`claude_cli_path: ${configPath2}`,
|
||||||
|
].join('\n');
|
||||||
|
writeFileSync(configPath, yaml, 'utf-8');
|
||||||
|
|
||||||
|
const path = resolveClaudeCliPath();
|
||||||
|
expect(path).toBe(envPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use project config when env var is not set', () => {
|
||||||
|
delete process.env['TAKT_CLAUDE_CLI_PATH'];
|
||||||
|
const projPath = createExecutableFile('project-claude');
|
||||||
|
|
||||||
|
const path = resolveClaudeCliPath({ claudeCliPath: projPath });
|
||||||
|
expect(path).toBe(projPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should prefer project config over global config', () => {
|
||||||
|
delete process.env['TAKT_CLAUDE_CLI_PATH'];
|
||||||
|
const projPath = createExecutableFile('project-claude');
|
||||||
|
const globalPath = createExecutableFile('global-claude');
|
||||||
|
const yaml = [
|
||||||
|
'language: en',
|
||||||
|
'log_level: info',
|
||||||
|
'provider: claude',
|
||||||
|
`claude_cli_path: ${globalPath}`,
|
||||||
|
].join('\n');
|
||||||
|
writeFileSync(configPath, yaml, 'utf-8');
|
||||||
|
|
||||||
|
const path = resolveClaudeCliPath({ claudeCliPath: projPath });
|
||||||
|
expect(path).toBe(projPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fall back to global config when neither env nor project is set', () => {
|
||||||
|
delete process.env['TAKT_CLAUDE_CLI_PATH'];
|
||||||
|
const globalPath = createExecutableFile('global-claude');
|
||||||
|
const yaml = [
|
||||||
|
'language: en',
|
||||||
|
'log_level: info',
|
||||||
|
'provider: claude',
|
||||||
|
`claude_cli_path: ${globalPath}`,
|
||||||
|
].join('\n');
|
||||||
|
writeFileSync(configPath, yaml, 'utf-8');
|
||||||
|
|
||||||
|
const path = resolveClaudeCliPath();
|
||||||
|
expect(path).toBe(globalPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return undefined when nothing is set', () => {
|
||||||
|
delete process.env['TAKT_CLAUDE_CLI_PATH'];
|
||||||
|
const yaml = [
|
||||||
|
'language: en',
|
||||||
|
'log_level: info',
|
||||||
|
'provider: claude',
|
||||||
|
].join('\n');
|
||||||
|
writeFileSync(configPath, yaml, 'utf-8');
|
||||||
|
|
||||||
|
const path = resolveClaudeCliPath();
|
||||||
|
expect(path).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw when env path is invalid', () => {
|
||||||
|
process.env['TAKT_CLAUDE_CLI_PATH'] = join(testDir, 'missing-claude');
|
||||||
|
expect(() => resolveClaudeCliPath()).toThrow(/does not exist/i);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('resolveCursorCliPath', () => {
|
||||||
|
const originalEnv = process.env['TAKT_CURSOR_CLI_PATH'];
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
invalidateGlobalConfigCache();
|
||||||
|
mkdirSync(taktDir, { recursive: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (originalEnv !== undefined) {
|
||||||
|
process.env['TAKT_CURSOR_CLI_PATH'] = originalEnv;
|
||||||
|
} else {
|
||||||
|
delete process.env['TAKT_CURSOR_CLI_PATH'];
|
||||||
|
}
|
||||||
|
rmSync(testDir, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return env var path when set (highest priority)', () => {
|
||||||
|
const envPath = createExecutableFile('env-cursor');
|
||||||
|
const configPath2 = createExecutableFile('config-cursor');
|
||||||
|
process.env['TAKT_CURSOR_CLI_PATH'] = envPath;
|
||||||
|
const yaml = [
|
||||||
|
'language: en',
|
||||||
|
'log_level: info',
|
||||||
|
'provider: cursor',
|
||||||
|
`cursor_cli_path: ${configPath2}`,
|
||||||
|
].join('\n');
|
||||||
|
writeFileSync(configPath, yaml, 'utf-8');
|
||||||
|
|
||||||
|
const path = resolveCursorCliPath();
|
||||||
|
expect(path).toBe(envPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use project config when env var is not set', () => {
|
||||||
|
delete process.env['TAKT_CURSOR_CLI_PATH'];
|
||||||
|
const projPath = createExecutableFile('project-cursor');
|
||||||
|
|
||||||
|
const path = resolveCursorCliPath({ cursorCliPath: projPath });
|
||||||
|
expect(path).toBe(projPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should prefer project config over global config', () => {
|
||||||
|
delete process.env['TAKT_CURSOR_CLI_PATH'];
|
||||||
|
const projPath = createExecutableFile('project-cursor');
|
||||||
|
const globalPath = createExecutableFile('global-cursor');
|
||||||
|
const yaml = [
|
||||||
|
'language: en',
|
||||||
|
'log_level: info',
|
||||||
|
'provider: cursor',
|
||||||
|
`cursor_cli_path: ${globalPath}`,
|
||||||
|
].join('\n');
|
||||||
|
writeFileSync(configPath, yaml, 'utf-8');
|
||||||
|
|
||||||
|
const path = resolveCursorCliPath({ cursorCliPath: projPath });
|
||||||
|
expect(path).toBe(projPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fall back to global config when neither env nor project is set', () => {
|
||||||
|
delete process.env['TAKT_CURSOR_CLI_PATH'];
|
||||||
|
const globalPath = createExecutableFile('global-cursor');
|
||||||
|
const yaml = [
|
||||||
|
'language: en',
|
||||||
|
'log_level: info',
|
||||||
|
'provider: cursor',
|
||||||
|
`cursor_cli_path: ${globalPath}`,
|
||||||
|
].join('\n');
|
||||||
|
writeFileSync(configPath, yaml, 'utf-8');
|
||||||
|
|
||||||
|
const path = resolveCursorCliPath();
|
||||||
|
expect(path).toBe(globalPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return undefined when nothing is set', () => {
|
||||||
|
delete process.env['TAKT_CURSOR_CLI_PATH'];
|
||||||
|
const yaml = [
|
||||||
|
'language: en',
|
||||||
|
'log_level: info',
|
||||||
|
'provider: cursor',
|
||||||
|
].join('\n');
|
||||||
|
writeFileSync(configPath, yaml, 'utf-8');
|
||||||
|
|
||||||
|
const path = resolveCursorCliPath();
|
||||||
|
expect(path).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw when env path is invalid', () => {
|
||||||
|
process.env['TAKT_CURSOR_CLI_PATH'] = join(testDir, 'missing-cursor');
|
||||||
|
expect(() => resolveCursorCliPath()).toThrow(/does not exist/i);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Task 6.3 — resolveCodexCliPath project config layer tests
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
describe('resolveCodexCliPath — project config layer', () => {
|
||||||
|
const originalEnv = process.env['TAKT_CODEX_CLI_PATH'];
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
invalidateGlobalConfigCache();
|
||||||
|
mkdirSync(taktDir, { recursive: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (originalEnv !== undefined) {
|
||||||
|
process.env['TAKT_CODEX_CLI_PATH'] = originalEnv;
|
||||||
|
} else {
|
||||||
|
delete process.env['TAKT_CODEX_CLI_PATH'];
|
||||||
|
}
|
||||||
|
rmSync(testDir, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use project config when env var is not set', () => {
|
||||||
|
delete process.env['TAKT_CODEX_CLI_PATH'];
|
||||||
|
const projPath = createExecutableFile('project-codex');
|
||||||
|
|
||||||
|
const path = resolveCodexCliPath({ codexCliPath: projPath });
|
||||||
|
expect(path).toBe(projPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should prefer env var over project config', () => {
|
||||||
|
const envPath = createExecutableFile('env-codex');
|
||||||
|
const projPath = createExecutableFile('project-codex');
|
||||||
|
process.env['TAKT_CODEX_CLI_PATH'] = envPath;
|
||||||
|
|
||||||
|
const path = resolveCodexCliPath({ codexCliPath: projPath });
|
||||||
|
expect(path).toBe(envPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should prefer project config over global config', () => {
|
||||||
|
delete process.env['TAKT_CODEX_CLI_PATH'];
|
||||||
|
const projPath = createExecutableFile('project-codex');
|
||||||
|
const globalPath = createExecutableFile('global-codex');
|
||||||
|
const yaml = [
|
||||||
|
'language: en',
|
||||||
|
'log_level: info',
|
||||||
|
'provider: codex',
|
||||||
|
`codex_cli_path: ${globalPath}`,
|
||||||
|
].join('\n');
|
||||||
|
writeFileSync(configPath, yaml, 'utf-8');
|
||||||
|
|
||||||
|
const path = resolveCodexCliPath({ codexCliPath: projPath });
|
||||||
|
expect(path).toBe(projPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw when project config path is invalid', () => {
|
||||||
|
delete process.env['TAKT_CODEX_CLI_PATH'];
|
||||||
|
expect(() => resolveCodexCliPath({ codexCliPath: join(testDir, 'missing-codex') }))
|
||||||
|
.toThrow(/does not exist/i);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@ -52,12 +52,14 @@ vi.mock('../infra/opencode/index.js', () => ({
|
|||||||
callOpenCodeCustom: mockCallOpenCodeCustom,
|
callOpenCodeCustom: mockCallOpenCodeCustom,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// ===== Config (API key resolvers) =====
|
// ===== Config (API key resolvers + CLI path resolvers) =====
|
||||||
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'),
|
resolveCodexCliPath: vi.fn(() => '/opt/codex/bin/codex'),
|
||||||
|
resolveClaudeCliPath: vi.fn(() => undefined),
|
||||||
resolveOpencodeApiKey: vi.fn(() => undefined),
|
resolveOpencodeApiKey: vi.fn(() => undefined),
|
||||||
|
loadProjectConfig: vi.fn(() => ({})),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Codex の isInsideGitRepo をバイパス
|
// Codex の isInsideGitRepo をバイパス
|
||||||
|
|||||||
@ -90,6 +90,10 @@ export interface PersistedGlobalConfig {
|
|||||||
openaiApiKey?: string;
|
openaiApiKey?: string;
|
||||||
/** External Codex CLI path for Codex SDK override (overridden by TAKT_CODEX_CLI_PATH env var) */
|
/** External Codex CLI path for Codex SDK override (overridden by TAKT_CODEX_CLI_PATH env var) */
|
||||||
codexCliPath?: string;
|
codexCliPath?: string;
|
||||||
|
/** External Claude Code CLI path (overridden by TAKT_CLAUDE_CLI_PATH env var) */
|
||||||
|
claudeCliPath?: string;
|
||||||
|
/** External cursor-agent CLI path (overridden by TAKT_CURSOR_CLI_PATH env var) */
|
||||||
|
cursorCliPath?: 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;
|
||||||
/** Cursor API key for Cursor Agent CLI/API (overridden by TAKT_CURSOR_API_KEY env var) */
|
/** Cursor API key for Cursor Agent CLI/API (overridden by TAKT_CURSOR_API_KEY env var) */
|
||||||
|
|||||||
@ -445,6 +445,10 @@ export const GlobalConfigSchema = z.object({
|
|||||||
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) */
|
/** External Codex CLI path for Codex SDK override (overridden by TAKT_CODEX_CLI_PATH env var) */
|
||||||
codex_cli_path: z.string().optional(),
|
codex_cli_path: z.string().optional(),
|
||||||
|
/** External Claude Code CLI path (overridden by TAKT_CLAUDE_CLI_PATH env var) */
|
||||||
|
claude_cli_path: z.string().optional(),
|
||||||
|
/** External cursor-agent CLI path (overridden by TAKT_CURSOR_CLI_PATH env var) */
|
||||||
|
cursor_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(),
|
||||||
/** Cursor API key for Cursor Agent CLI/API (overridden by TAKT_CURSOR_API_KEY env var) */
|
/** Cursor API key for Cursor Agent CLI/API (overridden by TAKT_CURSOR_API_KEY env var) */
|
||||||
@ -518,4 +522,10 @@ export const ProjectConfigSchema = z.object({
|
|||||||
]).optional(),
|
]).optional(),
|
||||||
/** Compatibility flag for full submodule acquisition when submodules is unset */
|
/** Compatibility flag for full submodule acquisition when submodules is unset */
|
||||||
with_submodules: z.boolean().optional(),
|
with_submodules: z.boolean().optional(),
|
||||||
|
/** Claude Code CLI path override (project-level) */
|
||||||
|
claude_cli_path: z.string().optional(),
|
||||||
|
/** Codex CLI path override (project-level) */
|
||||||
|
codex_cli_path: z.string().optional(),
|
||||||
|
/** cursor-agent CLI path override (project-level) */
|
||||||
|
cursor_cli_path: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -53,6 +53,7 @@ export class ClaudeClient {
|
|||||||
anthropicApiKey: options.anthropicApiKey,
|
anthropicApiKey: options.anthropicApiKey,
|
||||||
outputSchema: options.outputSchema,
|
outputSchema: options.outputSchema,
|
||||||
sandbox: options.sandbox,
|
sandbox: options.sandbox,
|
||||||
|
pathToClaudeCodeExecutable: options.pathToClaudeCodeExecutable,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -100,6 +100,10 @@ export class SdkOptionsBuilder {
|
|||||||
sdkOptions.sandbox = this.options.sandbox;
|
sdkOptions.sandbox = this.options.sandbox;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.options.pathToClaudeCodeExecutable) {
|
||||||
|
sdkOptions.pathToClaudeCodeExecutable = this.options.pathToClaudeCodeExecutable;
|
||||||
|
}
|
||||||
|
|
||||||
return sdkOptions;
|
return sdkOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -149,6 +149,8 @@ export interface ClaudeCallOptions {
|
|||||||
outputSchema?: Record<string, unknown>;
|
outputSchema?: Record<string, unknown>;
|
||||||
/** Sandbox settings for Claude SDK */
|
/** Sandbox settings for Claude SDK */
|
||||||
sandbox?: SandboxSettings;
|
sandbox?: SandboxSettings;
|
||||||
|
/** Custom path to Claude Code executable */
|
||||||
|
pathToClaudeCodeExecutable?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Options for spawning a Claude SDK query (low-level, used by executor/process) */
|
/** Options for spawning a Claude SDK query (low-level, used by executor/process) */
|
||||||
@ -182,4 +184,6 @@ export interface ClaudeSpawnOptions {
|
|||||||
onStderr?: (data: string) => void;
|
onStderr?: (data: string) => void;
|
||||||
/** Sandbox settings for Claude SDK */
|
/** Sandbox settings for Claude SDK */
|
||||||
sandbox?: SandboxSettings;
|
sandbox?: SandboxSettings;
|
||||||
|
/** Custom path to Claude Code executable */
|
||||||
|
pathToClaudeCodeExecutable?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
5
src/infra/config/env/config-env-overrides.ts
vendored
5
src/infra/config/env/config-env-overrides.ts
vendored
@ -94,6 +94,8 @@ const GLOBAL_ENV_SPECS: readonly EnvSpec[] = [
|
|||||||
{ path: 'anthropic_api_key', type: 'string' },
|
{ path: 'anthropic_api_key', type: 'string' },
|
||||||
{ path: 'openai_api_key', type: 'string' },
|
{ path: 'openai_api_key', type: 'string' },
|
||||||
{ path: 'codex_cli_path', type: 'string' },
|
{ path: 'codex_cli_path', type: 'string' },
|
||||||
|
{ path: 'claude_cli_path', type: 'string' },
|
||||||
|
{ path: 'cursor_cli_path', type: 'string' },
|
||||||
{ path: 'opencode_api_key', type: 'string' },
|
{ path: 'opencode_api_key', type: 'string' },
|
||||||
{ path: 'cursor_api_key', type: 'string' },
|
{ path: 'cursor_api_key', type: 'string' },
|
||||||
{ path: 'pipeline', type: 'json' },
|
{ path: 'pipeline', type: 'json' },
|
||||||
@ -145,6 +147,9 @@ const PROJECT_ENV_SPECS: readonly EnvSpec[] = [
|
|||||||
{ path: 'provider_options.claude.sandbox.excluded_commands', type: 'json' },
|
{ path: 'provider_options.claude.sandbox.excluded_commands', type: 'json' },
|
||||||
{ path: 'provider_profiles', type: 'json' },
|
{ path: 'provider_profiles', type: 'json' },
|
||||||
{ path: 'base_branch', type: 'string' },
|
{ path: 'base_branch', type: 'string' },
|
||||||
|
{ path: 'claude_cli_path', type: 'string' },
|
||||||
|
{ path: 'codex_cli_path', type: 'string' },
|
||||||
|
{ path: 'cursor_cli_path', type: 'string' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export function applyGlobalConfigEnvOverrides(target: Record<string, unknown>): void {
|
export function applyGlobalConfigEnvOverrides(target: Record<string, unknown>): void {
|
||||||
|
|||||||
@ -32,7 +32,8 @@ function hasControlCharacters(value: string): boolean {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateCodexCliPath(pathValue: string, sourceName: 'TAKT_CODEX_CLI_PATH' | 'codex_cli_path'): string {
|
/** Validate a CLI path value: must be non-empty, absolute, existing, executable file without control characters. */
|
||||||
|
export function validateCliPath(pathValue: string, sourceName: string): string {
|
||||||
const trimmed = pathValue.trim();
|
const trimmed = pathValue.trim();
|
||||||
if (trimmed.length === 0) {
|
if (trimmed.length === 0) {
|
||||||
throw new Error(`Configuration error: ${sourceName} must not be empty.`);
|
throw new Error(`Configuration error: ${sourceName} must not be empty.`);
|
||||||
@ -191,6 +192,8 @@ export class GlobalConfigManager {
|
|||||||
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,
|
codexCliPath: parsed.codex_cli_path,
|
||||||
|
claudeCliPath: parsed.claude_cli_path,
|
||||||
|
cursorCliPath: parsed.cursor_cli_path,
|
||||||
opencodeApiKey: parsed.opencode_api_key,
|
opencodeApiKey: parsed.opencode_api_key,
|
||||||
cursorApiKey: parsed.cursor_api_key,
|
cursorApiKey: parsed.cursor_api_key,
|
||||||
pipeline: parsed.pipeline ? {
|
pipeline: parsed.pipeline ? {
|
||||||
@ -278,6 +281,12 @@ export class GlobalConfigManager {
|
|||||||
if (config.codexCliPath) {
|
if (config.codexCliPath) {
|
||||||
raw.codex_cli_path = config.codexCliPath;
|
raw.codex_cli_path = config.codexCliPath;
|
||||||
}
|
}
|
||||||
|
if (config.claudeCliPath) {
|
||||||
|
raw.claude_cli_path = config.claudeCliPath;
|
||||||
|
}
|
||||||
|
if (config.cursorCliPath) {
|
||||||
|
raw.cursor_cli_path = config.cursorCliPath;
|
||||||
|
}
|
||||||
if (config.opencodeApiKey) {
|
if (config.opencodeApiKey) {
|
||||||
raw.opencode_api_key = config.opencodeApiKey;
|
raw.opencode_api_key = config.opencodeApiKey;
|
||||||
}
|
}
|
||||||
@ -456,10 +465,14 @@ export function resolveOpenaiApiKey(): string | undefined {
|
|||||||
* Resolve the Codex CLI path override.
|
* Resolve the Codex CLI path override.
|
||||||
* Priority: TAKT_CODEX_CLI_PATH env var > config.yaml > undefined (SDK vendored binary fallback)
|
* Priority: TAKT_CODEX_CLI_PATH env var > config.yaml > undefined (SDK vendored binary fallback)
|
||||||
*/
|
*/
|
||||||
export function resolveCodexCliPath(): string | undefined {
|
export function resolveCodexCliPath(projectConfig?: { codexCliPath?: string }): string | undefined {
|
||||||
const envPath = process.env[envVarNameFromPath('codex_cli_path')];
|
const envPath = process.env[envVarNameFromPath('codex_cli_path')];
|
||||||
if (envPath !== undefined) {
|
if (envPath !== undefined) {
|
||||||
return validateCodexCliPath(envPath, 'TAKT_CODEX_CLI_PATH');
|
return validateCliPath(envPath, 'TAKT_CODEX_CLI_PATH');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (projectConfig?.codexCliPath !== undefined) {
|
||||||
|
return validateCliPath(projectConfig.codexCliPath, 'codex_cli_path (project)');
|
||||||
}
|
}
|
||||||
|
|
||||||
let config: PersistedGlobalConfig;
|
let config: PersistedGlobalConfig;
|
||||||
@ -471,7 +484,59 @@ export function resolveCodexCliPath(): string | undefined {
|
|||||||
if (config.codexCliPath === undefined) {
|
if (config.codexCliPath === undefined) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
return validateCodexCliPath(config.codexCliPath, 'codex_cli_path');
|
return validateCliPath(config.codexCliPath, 'codex_cli_path');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve the Claude Code CLI path override.
|
||||||
|
* Priority: TAKT_CLAUDE_CLI_PATH env var > project config > global config > undefined (SDK default)
|
||||||
|
*/
|
||||||
|
export function resolveClaudeCliPath(projectConfig?: { claudeCliPath?: string }): string | undefined {
|
||||||
|
const envPath = process.env[envVarNameFromPath('claude_cli_path')];
|
||||||
|
if (envPath !== undefined) {
|
||||||
|
return validateCliPath(envPath, 'TAKT_CLAUDE_CLI_PATH');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (projectConfig?.claudeCliPath !== undefined) {
|
||||||
|
return validateCliPath(projectConfig.claudeCliPath, 'claude_cli_path (project)');
|
||||||
|
}
|
||||||
|
|
||||||
|
let config: PersistedGlobalConfig;
|
||||||
|
try {
|
||||||
|
config = loadGlobalConfig();
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (config.claudeCliPath === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return validateCliPath(config.claudeCliPath, 'claude_cli_path');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve the cursor-agent CLI path override.
|
||||||
|
* Priority: TAKT_CURSOR_CLI_PATH env var > project config > global config > undefined (default 'cursor-agent')
|
||||||
|
*/
|
||||||
|
export function resolveCursorCliPath(projectConfig?: { cursorCliPath?: string }): string | undefined {
|
||||||
|
const envPath = process.env[envVarNameFromPath('cursor_cli_path')];
|
||||||
|
if (envPath !== undefined) {
|
||||||
|
return validateCliPath(envPath, 'TAKT_CURSOR_CLI_PATH');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (projectConfig?.cursorCliPath !== undefined) {
|
||||||
|
return validateCliPath(projectConfig.cursorCliPath, 'cursor_cli_path (project)');
|
||||||
|
}
|
||||||
|
|
||||||
|
let config: PersistedGlobalConfig;
|
||||||
|
try {
|
||||||
|
config = loadGlobalConfig();
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (config.cursorCliPath === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return validateCliPath(config.cursorCliPath, 'cursor_cli_path');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -15,8 +15,11 @@ export {
|
|||||||
resolveAnthropicApiKey,
|
resolveAnthropicApiKey,
|
||||||
resolveOpenaiApiKey,
|
resolveOpenaiApiKey,
|
||||||
resolveCodexCliPath,
|
resolveCodexCliPath,
|
||||||
|
resolveClaudeCliPath,
|
||||||
|
resolveCursorCliPath,
|
||||||
resolveOpencodeApiKey,
|
resolveOpencodeApiKey,
|
||||||
resolveCursorApiKey,
|
resolveCursorApiKey,
|
||||||
|
validateCliPath,
|
||||||
} from './globalConfig.js';
|
} from './globalConfig.js';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|||||||
@ -157,6 +157,9 @@ export function loadProjectConfig(projectDir: string): ProjectLocalConfig {
|
|||||||
provider_options,
|
provider_options,
|
||||||
provider_profiles,
|
provider_profiles,
|
||||||
analytics,
|
analytics,
|
||||||
|
claude_cli_path,
|
||||||
|
codex_cli_path,
|
||||||
|
cursor_cli_path,
|
||||||
...rest
|
...rest
|
||||||
} = parsedConfig;
|
} = parsedConfig;
|
||||||
|
|
||||||
@ -184,6 +187,9 @@ export function loadProjectConfig(projectDir: string): ProjectLocalConfig {
|
|||||||
};
|
};
|
||||||
} | undefined),
|
} | undefined),
|
||||||
providerProfiles: normalizeProviderProfiles(provider_profiles as Record<string, { default_permission_mode: unknown; movement_permission_overrides?: Record<string, unknown> }> | undefined),
|
providerProfiles: normalizeProviderProfiles(provider_profiles as Record<string, { default_permission_mode: unknown; movement_permission_overrides?: Record<string, unknown> }> | undefined),
|
||||||
|
claudeCliPath: claude_cli_path as string | undefined,
|
||||||
|
codexCliPath: codex_cli_path as string | undefined,
|
||||||
|
cursorCliPath: cursor_cli_path as string | undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -34,6 +34,12 @@ export interface ProjectLocalConfig {
|
|||||||
providerOptions?: MovementProviderOptions;
|
providerOptions?: MovementProviderOptions;
|
||||||
/** Provider-specific permission profiles (project-level override) */
|
/** Provider-specific permission profiles (project-level override) */
|
||||||
providerProfiles?: ProviderPermissionProfiles;
|
providerProfiles?: ProviderPermissionProfiles;
|
||||||
|
/** Claude Code CLI path override (project-level) */
|
||||||
|
claudeCliPath?: string;
|
||||||
|
/** Codex CLI path override (project-level) */
|
||||||
|
codexCliPath?: string;
|
||||||
|
/** cursor-agent CLI path override (project-level) */
|
||||||
|
cursorCliPath?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Persona session data for persistence */
|
/** Persona session data for persistence */
|
||||||
|
|||||||
@ -101,7 +101,7 @@ function createExecError(
|
|||||||
|
|
||||||
function execCursor(args: string[], options: CursorCallOptions): Promise<CursorExecResult> {
|
function execCursor(args: string[], options: CursorCallOptions): Promise<CursorExecResult> {
|
||||||
return new Promise<CursorExecResult>((resolve, reject) => {
|
return new Promise<CursorExecResult>((resolve, reject) => {
|
||||||
const child = spawn(CURSOR_COMMAND, args, {
|
const child = spawn(options.cursorCliPath ?? CURSOR_COMMAND, args, {
|
||||||
cwd: options.cwd,
|
cwd: options.cwd,
|
||||||
env: buildEnv(options.cursorApiKey),
|
env: buildEnv(options.cursorApiKey),
|
||||||
stdio: ['ignore', 'pipe', 'pipe'],
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
|
|||||||
@ -15,4 +15,6 @@ export interface CursorCallOptions {
|
|||||||
permissionMode?: PermissionMode;
|
permissionMode?: PermissionMode;
|
||||||
onStream?: StreamCallback;
|
onStream?: StreamCallback;
|
||||||
cursorApiKey?: string;
|
cursorApiKey?: string;
|
||||||
|
/** Custom path to cursor-agent executable */
|
||||||
|
cursorCliPath?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,12 +4,13 @@
|
|||||||
|
|
||||||
import { callClaude, callClaudeCustom, callClaudeAgent, callClaudeSkill } from '../claude/client.js';
|
import { callClaude, callClaudeCustom, callClaudeAgent, callClaudeSkill } from '../claude/client.js';
|
||||||
import type { ClaudeCallOptions } from '../claude/types.js';
|
import type { ClaudeCallOptions } from '../claude/types.js';
|
||||||
import { resolveAnthropicApiKey } from '../config/index.js';
|
import { resolveAnthropicApiKey, resolveClaudeCliPath, loadProjectConfig } 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';
|
||||||
|
|
||||||
function toClaudeOptions(options: ProviderCallOptions): ClaudeCallOptions {
|
function toClaudeOptions(options: ProviderCallOptions): ClaudeCallOptions {
|
||||||
const claudeSandbox = options.providerOptions?.claude?.sandbox;
|
const claudeSandbox = options.providerOptions?.claude?.sandbox;
|
||||||
|
const projectConfig = loadProjectConfig(options.cwd);
|
||||||
return {
|
return {
|
||||||
cwd: options.cwd,
|
cwd: options.cwd,
|
||||||
abortSignal: options.abortSignal,
|
abortSignal: options.abortSignal,
|
||||||
@ -29,6 +30,7 @@ function toClaudeOptions(options: ProviderCallOptions): ClaudeCallOptions {
|
|||||||
allowUnsandboxedCommands: claudeSandbox.allowUnsandboxedCommands,
|
allowUnsandboxedCommands: claudeSandbox.allowUnsandboxedCommands,
|
||||||
excludedCommands: claudeSandbox.excludedCommands,
|
excludedCommands: claudeSandbox.excludedCommands,
|
||||||
} : undefined,
|
} : undefined,
|
||||||
|
pathToClaudeCodeExecutable: resolveClaudeCliPath(projectConfig),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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, resolveCodexCliPath } from '../config/index.js';
|
import { resolveOpenaiApiKey, resolveCodexCliPath, loadProjectConfig } 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';
|
||||||
|
|
||||||
@ -25,6 +25,7 @@ function isInsideGitRepo(cwd: string): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function toCodexOptions(options: ProviderCallOptions): CodexCallOptions {
|
function toCodexOptions(options: ProviderCallOptions): CodexCallOptions {
|
||||||
|
const projectConfig = loadProjectConfig(options.cwd);
|
||||||
return {
|
return {
|
||||||
cwd: options.cwd,
|
cwd: options.cwd,
|
||||||
abortSignal: options.abortSignal,
|
abortSignal: options.abortSignal,
|
||||||
@ -34,7 +35,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(),
|
codexPathOverride: resolveCodexCliPath(projectConfig),
|
||||||
outputSchema: options.outputSchema,
|
outputSchema: options.outputSchema,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { callCursor, callCursorCustom, type CursorCallOptions } from '../cursor/index.js';
|
import { callCursor, callCursorCustom, type CursorCallOptions } from '../cursor/index.js';
|
||||||
import { resolveCursorApiKey } from '../config/index.js';
|
import { resolveCursorApiKey, resolveCursorCliPath, loadProjectConfig } from '../config/index.js';
|
||||||
import { createLogger } from '../../shared/utils/index.js';
|
import { createLogger } from '../../shared/utils/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';
|
||||||
@ -21,6 +21,7 @@ function toCursorOptions(options: ProviderCallOptions): CursorCallOptions {
|
|||||||
log.info('Cursor provider does not support outputSchema; ignoring');
|
log.info('Cursor provider does not support outputSchema; ignoring');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const projectConfig = loadProjectConfig(options.cwd);
|
||||||
return {
|
return {
|
||||||
cwd: options.cwd,
|
cwd: options.cwd,
|
||||||
abortSignal: options.abortSignal,
|
abortSignal: options.abortSignal,
|
||||||
@ -29,6 +30,7 @@ function toCursorOptions(options: ProviderCallOptions): CursorCallOptions {
|
|||||||
permissionMode: options.permissionMode,
|
permissionMode: options.permissionMode,
|
||||||
onStream: options.onStream,
|
onStream: options.onStream,
|
||||||
cursorApiKey: options.cursorApiKey ?? resolveCursorApiKey(),
|
cursorApiKey: options.cursorApiKey ?? resolveCursorApiKey(),
|
||||||
|
cursorCliPath: resolveCursorCliPath(projectConfig),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user