diff --git a/.changeset/cyan-worms-count.md b/.changeset/cyan-worms-count.md deleted file mode 100644 index 1584bb6c..00000000 --- a/.changeset/cyan-worms-count.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"task-master-ai": patch ---- - -Fix parse-prd schema to accept responses from models that omit optional fields (like Z.ai/GLM). Changed `metadata` field to use union pattern with `.default(null)` for better structured outputs compatibility. diff --git a/.changeset/grumpy-signs-type.md b/.changeset/grumpy-signs-type.md deleted file mode 100644 index 1f2a5e5b..00000000 --- a/.changeset/grumpy-signs-type.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"task-master-ai": patch ---- - -Fix ai response not showing price after its json was repaired diff --git a/.changeset/proxy-support-for-ai-providers.md b/.changeset/proxy-support-for-ai-providers.md new file mode 100644 index 00000000..38755856 --- /dev/null +++ b/.changeset/proxy-support-for-ai-providers.md @@ -0,0 +1,47 @@ +--- +"task-master-ai": patch +--- + +Added opt-in proxy support for all AI providers - respects http_proxy/https_proxy environment variables when enabled. + +When using Task Master in corporate or restricted network environments that require HTTP/HTTPS proxies, API calls to AI providers (OpenAI, Anthropic, Google, AWS Bedrock, etc.) would previously fail with ECONNRESET errors. This update adds seamless proxy support that can be enabled via environment variable or configuration file. + +**How to enable:** + +Proxy support is opt-in. Enable it using either method: + +**Method 1: Environment Variable** +```bash +export TASKMASTER_ENABLE_PROXY=true +export http_proxy=http://your-proxy:port +export https_proxy=http://your-proxy:port +export no_proxy=localhost,127.0.0.1 # Optional: bypass proxy for specific hosts + +# Then use Task Master normally +task-master add-task "Create a new feature" +``` + +**Method 2: Configuration File** + +Add to `.taskmaster/config.json`: +```json +{ + "global": { + "enableProxy": true + } +} +``` + +Then set your proxy environment variables: +```bash +export http_proxy=http://your-proxy:port +export https_proxy=http://your-proxy:port +``` + +**Technical details:** + +- Uses undici's `EnvHttpProxyAgent` for automatic proxy detection +- Centralized implementation in `BaseAIProvider` for consistency across all providers +- Supports all AI providers: OpenAI, Anthropic, Perplexity, Azure OpenAI, Google AI, Google Vertex AI, AWS Bedrock, and OpenAI-compatible providers +- Opt-in design ensures users without proxy requirements are not affected +- Priority: `TASKMASTER_ENABLE_PROXY` environment variable > `config.json` setting diff --git a/.changeset/ready-cities-marry.md b/.changeset/ready-cities-marry.md deleted file mode 100644 index c9a9fa07..00000000 --- a/.changeset/ready-cities-marry.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"task-master-ai": patch ---- - -Enable structured outputs for Z.ai providers. Added `supportsStructuredOutputs: true` to use `json_schema` mode for more reliable JSON generation in operations like parse-prd. diff --git a/.env.example b/.env.example index 1a261b92..0da07875 100644 --- a/.env.example +++ b/.env.example @@ -14,4 +14,9 @@ OLLAMA_API_KEY=YOUR_OLLAMA_API_KEY_HERE VERTEX_PROJECT_ID=your-gcp-project-id VERTEX_LOCATION=us-central1 # Optional: Path to service account credentials JSON file (alternative to API key) -GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account-credentials.json \ No newline at end of file +GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account-credentials.json + +# Proxy Configuration (Optional) +# Enable proxy support for AI provider API calls +# When enabled, automatically uses http_proxy/https_proxy environment variables +TASKMASTER_ENABLE_PROXY=false diff --git a/CHANGELOG.md b/CHANGELOG.md index 29c42a7d..afee84fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # task-master-ai +## 0.31.2 + +### Patch Changes + +- [#1377](https://github.com/eyaltoledano/claude-task-master/pull/1377) [`3c22875`](https://github.com/eyaltoledano/claude-task-master/commit/3c22875efeb5d21754d447a9559817bc5327a234) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Fix parse-prd schema to accept responses from models that omit optional fields (like Z.ai/GLM). Changed `metadata` field to use union pattern with `.default(null)` for better structured outputs compatibility. + +- [#1377](https://github.com/eyaltoledano/claude-task-master/pull/1377) [`3c22875`](https://github.com/eyaltoledano/claude-task-master/commit/3c22875efeb5d21754d447a9559817bc5327a234) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Fix ai response not showing price after its json was repaired + +- [#1377](https://github.com/eyaltoledano/claude-task-master/pull/1377) [`3c22875`](https://github.com/eyaltoledano/claude-task-master/commit/3c22875efeb5d21754d447a9559817bc5327a234) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Enable structured outputs for Z.ai providers. Added `supportsStructuredOutputs: true` to use `json_schema` mode for more reliable JSON generation in operations like parse-prd. + ## 0.31.1 ### Patch Changes diff --git a/package-lock.json b/package-lock.json index e4861c2f..1343084b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "task-master-ai", - "version": "0.31.1", + "version": "0.31.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "task-master-ai", - "version": "0.31.1", + "version": "0.31.2", "license": "MIT WITH Commons-Clause", "workspaces": [ "apps/*", @@ -66,6 +66,7 @@ "ora": "^8.2.0", "simple-git": "^3.28.0", "steno": "^4.0.2", + "undici": "^7.16.0", "uuid": "^11.1.0", "zod": "^4.1.11" }, @@ -26530,6 +26531,8 @@ }, "node_modules/undici": { "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.16.0.tgz", + "integrity": "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==", "license": "MIT", "engines": { "node": ">=20.18.1" @@ -28272,7 +28275,8 @@ "dependencies": { "@tm/core": "*", "boxen": "^8.0.1", - "chalk": "5.6.2" + "chalk": "5.6.2", + "ora": "^8.1.1" }, "devDependencies": { "@types/node": "^22.10.5", diff --git a/package.json b/package.json index 5917968d..a00edb6e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "task-master-ai", - "version": "0.31.1", + "version": "0.31.2", "description": "A task management system for ambitious AI-driven development that doesn't overwhelm and confuse Cursor.", "main": "index.js", "type": "module", @@ -104,6 +104,7 @@ "ora": "^8.2.0", "simple-git": "^3.28.0", "steno": "^4.0.2", + "undici": "^7.16.0", "uuid": "^11.1.0", "zod": "^4.1.11" }, diff --git a/scripts/modules/config-manager.js b/scripts/modules/config-manager.js index e75949b2..37b390b2 100644 --- a/scripts/modules/config-manager.js +++ b/scripts/modules/config-manager.js @@ -55,7 +55,8 @@ const DEFAULTS = { ollamaBaseURL: 'http://localhost:11434/api', bedrockBaseURL: 'https://bedrock.us-east-1.amazonaws.com', responseLanguage: 'English', - enableCodebaseAnalysis: true + enableCodebaseAnalysis: true, + enableProxy: false }, claudeCode: {}, codexCli: {}, @@ -703,6 +704,32 @@ function getCodebaseAnalysisEnabled(explicitRoot = null) { return getGlobalConfig(explicitRoot).enableCodebaseAnalysis !== false; } +function getProxyEnabled(explicitRoot = null) { + // Return boolean-safe value with default false + return getGlobalConfig(explicitRoot).enableProxy === true; +} + +function isProxyEnabled(session = null, projectRoot = null) { + // Priority 1: Environment variable + const envFlag = resolveEnvVariable( + 'TASKMASTER_ENABLE_PROXY', + session, + projectRoot + ); + if (envFlag !== null && envFlag !== undefined && envFlag !== '') { + return envFlag.toLowerCase() === 'true' || envFlag === '1'; + } + + // Priority 2: MCP session environment (explicit check for parity with other flags) + if (session?.env?.TASKMASTER_ENABLE_PROXY) { + const mcpFlag = session.env.TASKMASTER_ENABLE_PROXY; + return mcpFlag.toLowerCase() === 'true' || mcpFlag === '1'; + } + + // Priority 3: Configuration file + return getProxyEnabled(projectRoot); +} + /** * Gets model parameters (maxTokens, temperature) for a specific role, * considering model-specific overrides from supported-models.json. @@ -807,9 +834,9 @@ function isApiKeySet(providerName, session = null, projectRoot = null) { const providersWithoutApiKeys = [ CUSTOM_PROVIDERS.OLLAMA, CUSTOM_PROVIDERS.BEDROCK, - CUSTOM_PROVIDERS.MCP, CUSTOM_PROVIDERS.GEMINI_CLI, CUSTOM_PROVIDERS.GROK_CLI, + CUSTOM_PROVIDERS.MCP, CUSTOM_PROVIDERS.CODEX_CLI ]; @@ -1188,6 +1215,8 @@ export { getResponseLanguage, getCodebaseAnalysisEnabled, isCodebaseAnalysisEnabled, + getProxyEnabled, + isProxyEnabled, getParametersForRole, getUserId, // API Key Checkers (still relevant) diff --git a/src/ai-providers/anthropic.js b/src/ai-providers/anthropic.js index 4270924f..1791ac65 100644 --- a/src/ai-providers/anthropic.js +++ b/src/ai-providers/anthropic.js @@ -52,7 +52,8 @@ export class AnthropicAIProvider extends BaseAIProvider { ...(baseURL && { baseURL }), headers: { 'anthropic-beta': 'output-128k-2025-02-19' - } + }, + fetch: this.createProxyFetch() }); } catch (error) { this.handleError('client initialization', error); diff --git a/src/ai-providers/azure.js b/src/ai-providers/azure.js index 105e647b..4d3ea62e 100644 --- a/src/ai-providers/azure.js +++ b/src/ai-providers/azure.js @@ -51,7 +51,8 @@ export class AzureProvider extends BaseAIProvider { return createAzure({ apiKey, - baseURL + baseURL, + fetch: this.createProxyFetch() }); } catch (error) { this.handleError('client initialization', error); diff --git a/src/ai-providers/base-provider.js b/src/ai-providers/base-provider.js index 910bb1ce..f80ead00 100644 --- a/src/ai-providers/base-provider.js +++ b/src/ai-providers/base-provider.js @@ -8,7 +8,9 @@ import { NoObjectGeneratedError } from 'ai'; import { jsonrepair } from 'jsonrepair'; -import { log } from '../../scripts/modules/utils.js'; +import { log, findProjectRoot } from '../../scripts/modules/utils.js'; +import { isProxyEnabled } from '../../scripts/modules/config-manager.js'; +import { EnvHttpProxyAgent } from 'undici'; /** * Base class for all AI providers @@ -22,6 +24,9 @@ export class BaseAIProvider { // Each provider must set their name this.name = this.constructor.name; + // Cache proxy agent to avoid creating multiple instances + this._proxyAgent = null; + /** * Whether this provider needs explicit schema in JSON mode * Can be overridden by subclasses @@ -48,6 +53,37 @@ export class BaseAIProvider { } } + /** + * Creates a custom fetch function with proxy support. + * Only enables proxy when TASKMASTER_ENABLE_PROXY environment variable is set to 'true' + * or enableProxy is set to true in config.json. + * Automatically reads http_proxy/https_proxy environment variables when enabled. + * @returns {Function} Custom fetch function with proxy support, or undefined if proxy is disabled + */ + createProxyFetch() { + // Cache project root to avoid repeated lookups + if (!this._projectRoot) { + this._projectRoot = findProjectRoot(); + } + const projectRoot = this._projectRoot; + + if (!isProxyEnabled(null, projectRoot)) { + // Return undefined to use default fetch without proxy + return undefined; + } + + // Proxy is enabled, create and return proxy fetch + if (!this._proxyAgent) { + this._proxyAgent = new EnvHttpProxyAgent(); + } + return (url, options = {}) => { + return fetch(url, { + ...options, + dispatcher: this._proxyAgent + }); + }; + } + /** * Validates common parameters across all methods * @param {object} params - Parameters to validate diff --git a/src/ai-providers/bedrock.js b/src/ai-providers/bedrock.js index 39b46d50..6c212410 100644 --- a/src/ai-providers/bedrock.js +++ b/src/ai-providers/bedrock.js @@ -37,7 +37,8 @@ export class BedrockAIProvider extends BaseAIProvider { const credentialProvider = fromNodeProviderChain(); return createAmazonBedrock({ - credentialProvider + credentialProvider, + fetch: this.createProxyFetch() }); } catch (error) { this.handleError('client initialization', error); diff --git a/src/ai-providers/google-vertex.js b/src/ai-providers/google-vertex.js index 57718f93..29469548 100644 --- a/src/ai-providers/google-vertex.js +++ b/src/ai-providers/google-vertex.js @@ -109,7 +109,8 @@ export class VertexAIProvider extends BaseAIProvider { ...authOptions, projectId, location, - ...(baseURL && { baseURL }) + ...(baseURL && { baseURL }), + fetch: this.createProxyFetch() }); } catch (error) { this.handleError('client initialization', error); diff --git a/src/ai-providers/google.js b/src/ai-providers/google.js index eef24b07..d0349d44 100644 --- a/src/ai-providers/google.js +++ b/src/ai-providers/google.js @@ -38,7 +38,8 @@ export class GoogleAIProvider extends BaseAIProvider { return createGoogleGenerativeAI({ apiKey, - ...(baseURL && { baseURL }) + ...(baseURL && { baseURL }), + fetch: this.createProxyFetch() }); } catch (error) { this.handleError('client initialization', error); diff --git a/src/ai-providers/openai-compatible.js b/src/ai-providers/openai-compatible.js index c0bb72ba..3621dccd 100644 --- a/src/ai-providers/openai-compatible.js +++ b/src/ai-providers/openai-compatible.js @@ -106,7 +106,9 @@ export class OpenAICompatibleProvider extends BaseAIProvider { const clientConfig = { // Provider name for SDK (required, used for logging/debugging) - name: this.name.toLowerCase().replace(/[^a-z0-9]/g, '-') + name: this.name.toLowerCase().replace(/[^a-z0-9]/g, '-'), + // Add proxy support + fetch: this.createProxyFetch() }; // Only include apiKey if provider requires it diff --git a/src/ai-providers/openai.js b/src/ai-providers/openai.js index 4f7b67eb..755b62ec 100644 --- a/src/ai-providers/openai.js +++ b/src/ai-providers/openai.js @@ -38,7 +38,8 @@ export class OpenAIProvider extends BaseAIProvider { return createOpenAI({ apiKey, - ...(baseURL && { baseURL }) + ...(baseURL && { baseURL }), + fetch: this.createProxyFetch() }); } catch (error) { this.handleError('client initialization', error); diff --git a/src/ai-providers/perplexity.js b/src/ai-providers/perplexity.js index 41ec4528..87df794a 100644 --- a/src/ai-providers/perplexity.js +++ b/src/ai-providers/perplexity.js @@ -38,7 +38,8 @@ export class PerplexityAIProvider extends BaseAIProvider { return createPerplexity({ apiKey, - baseURL: baseURL || 'https://api.perplexity.ai' + baseURL: baseURL || 'https://api.perplexity.ai', + fetch: this.createProxyFetch() }); } catch (error) { this.handleError('client initialization', error); diff --git a/tests/unit/ai-providers/zai-provider.test.js b/tests/unit/ai-providers/zai-provider.test.js index eb34acaf..1532bf9c 100644 --- a/tests/unit/ai-providers/zai-provider.test.js +++ b/tests/unit/ai-providers/zai-provider.test.js @@ -10,7 +10,13 @@ jest.unstable_mockModule('@ai-sdk/openai-compatible', () => ({ // Mock utils jest.unstable_mockModule('../../../scripts/modules/utils.js', () => ({ log: jest.fn(), - resolveEnvVariable: jest.fn((key) => process.env[key]) + resolveEnvVariable: jest.fn((key) => process.env[key]), + findProjectRoot: jest.fn(() => process.cwd()), + isEmpty: jest.fn(() => false) +})); + +jest.unstable_mockModule('../../../scripts/modules/config-manager.js', () => ({ + isProxyEnabled: jest.fn(() => false) })); // Import after mocking diff --git a/tests/unit/config-manager.test.js b/tests/unit/config-manager.test.js index 84856de5..074a668a 100644 --- a/tests/unit/config-manager.test.js +++ b/tests/unit/config-manager.test.js @@ -32,7 +32,7 @@ jest.mock('chalk', () => ({ blue: jest.fn((text) => text), green: jest.fn((text) => text), yellow: jest.fn((text) => text), - white: jest.fn((text) => ({ + white: jest.fn(() => ({ bold: jest.fn((text) => text) })), reset: jest.fn((text) => text), @@ -70,13 +70,13 @@ const realSupportedModelsPath = path.resolve( '../../scripts/modules/supported-models.json' ); let REAL_SUPPORTED_MODELS_CONTENT; -let REAL_SUPPORTED_MODELS_DATA; +let _REAL_SUPPORTED_MODELS_DATA; try { REAL_SUPPORTED_MODELS_CONTENT = fs.readFileSync( realSupportedModelsPath, 'utf-8' ); - REAL_SUPPORTED_MODELS_DATA = JSON.parse(REAL_SUPPORTED_MODELS_CONTENT); + _REAL_SUPPORTED_MODELS_DATA = JSON.parse(REAL_SUPPORTED_MODELS_CONTENT); } catch (err) { console.error( 'FATAL TEST SETUP ERROR: Could not read or parse real supported-models.json', @@ -146,6 +146,7 @@ const DEFAULT_CONFIG = { ollamaBaseURL: 'http://localhost:11434/api', bedrockBaseURL: 'https://bedrock.us-east-1.amazonaws.com', enableCodebaseAnalysis: true, + enableProxy: false, responseLanguage: 'English' }, claudeCode: {},