chore: apply requested changes

This commit is contained in:
Ralph Khreish
2025-09-29 16:23:37 +02:00
parent 986ac117ae
commit 100c3dc47d
17 changed files with 3052 additions and 2511 deletions

View File

@@ -35,7 +35,7 @@
"@types/inquirer": "^9.0.3",
"@types/node": "^22.10.5",
"tsx": "^4.20.4",
"typescript": "^5.7.3",
"typescript": "^5.9.2",
"vitest": "^2.1.8"
},
"engines": {

View File

@@ -240,7 +240,7 @@
"check-types": "tsc --noEmit"
},
"dependencies": {
"task-master-ai": "0.27.2"
"task-master-ai": "*"
},
"devDependencies": {
"@dnd-kit/core": "^6.3.1",
@@ -276,7 +276,7 @@
"react-dom": "^19.0.0",
"tailwind-merge": "^3.3.1",
"tailwindcss": "4.1.11",
"typescript": "^5.7.3"
"typescript": "^5.9.2"
},
"overrides": {
"glob@<8": "^10.4.5",

View File

@@ -15,12 +15,14 @@ The Claude Code provider uses token authentication managed by the Claude Code CL
### Prerequisites
1. **Install Claude Code CLI** (if not already installed):
```bash
# Installation method depends on your system
# Follow Claude Code documentation for installation
```
2. **Set up OAuth token** using Claude Code CLI:
```bash
claude setup-token
```
@@ -75,6 +77,7 @@ export CLAUDE_CODE_OAUTH_TOKEN="your_oauth_token_here"
```
This is only needed in specific scenarios like:
- CI/CD pipelines
- Docker containers
- When you want to use a different token than the CLI default
@@ -110,6 +113,7 @@ task-master models --setup
**Problem**: Task Master cannot connect to Claude Code CLI.
**Solutions**:
- Ensure Claude Code CLI is installed and in your PATH
- Run `claude setup-token` to configure authentication
- Verify Claude Code CLI works: `claude --help`
@@ -119,6 +123,7 @@ task-master models --setup
**Problem**: Token authentication is failing.
**Solutions**:
- Re-run `claude setup-token` to refresh your OAuth token
- Check if your token has expired
- Verify Claude Code CLI can authenticate: try a simple `claude` command
@@ -128,6 +133,7 @@ task-master models --setup
**Problem**: Specified Claude Code model is not supported.
**Solutions**:
- Use supported models: `sonnet` or `opus`
- Check model availability: `task-master models --list`
- Verify your Claude Code CLI has access to the requested model
@@ -135,18 +141,21 @@ task-master models --setup
### Debug Steps
1. **Test Claude Code CLI directly**:
```bash
claude --help
# Should show help without errors
```
2. **Test authentication**:
```bash
claude setup-token --verify
# Should confirm token is valid
```
3. **Test Task Master integration**:
```bash
task-master models --test claude-code:sonnet
# Should successfully connect and test the model
@@ -164,6 +173,7 @@ When running in Docker, you'll need to:
1. Install Claude Code CLI in your container
2. Set up authentication via environment variable:
```dockerfile
ENV CLAUDE_CODE_OAUTH_TOKEN="your_token_here"
```
@@ -218,4 +228,4 @@ If you encounter issues:
1. Check the Claude Code CLI documentation
2. Verify your authentication setup with `claude setup-token --verify`
3. Review Task Master logs for detailed error messages
4. Open an issue with both Task Master and Claude Code version information
4. Open an issue with both Task Master and Claude Code version information

5328
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -127,8 +127,8 @@
"@changesets/changelog-github": "^0.5.1",
"@changesets/cli": "^2.28.1",
"@manypkg/cli": "^0.25.1",
"@tm/cli": "*",
"@tm/ai-sdk-provider-grok-cli": "*",
"@tm/cli": "*",
"@types/jest": "^29.5.14",
"@types/marked-terminal": "^6.1.1",
"concurrently": "^9.2.1",
@@ -143,7 +143,7 @@
"ts-jest": "^29.4.2",
"tsdown": "^0.15.2",
"tsx": "^4.20.4",
"turbo": "^2.5.6",
"typescript": "^5.7.3"
"turbo": "2.5.6",
"typescript": "^5.9.2"
}
}

View File

@@ -2,16 +2,12 @@
"name": "@tm/ai-sdk-provider-grok-cli",
"private": true,
"description": "AI SDK provider for Grok CLI integration",
"keywords": ["ai", "grok", "x.ai", "cli", "language-model", "provider"],
"license": "MIT",
"sideEffects": false,
"type": "module",
"types": "./src/index.ts",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"exports": {
".": "./src/index.ts"
},
"files": ["dist/**/*", "README.md"],
"scripts": {
"test": "vitest run",
"test:watch": "vitest",
@@ -28,14 +24,11 @@
"typescript": "^5.9.2",
"vitest": "^3.2.4"
},
"peerDependencies": {
"@ai-sdk/provider": "^2.0.0",
"@ai-sdk/provider-utils": "^3.0.10",
"jsonc-parser": "^3.3.1"
},
"engines": {
"node": ">=18"
},
"keywords": ["ai", "grok", "x.ai", "cli", "language-model", "provider"],
"files": ["dist/**/*", "README.md"],
"publishConfig": {
"access": "public"
}

View File

@@ -110,7 +110,7 @@ export class GrokCliLanguageModel implements LanguageModelV2 {
*/
private async executeGrokCli(
args: string[],
options: { timeout?: number } = {}
options: { timeout?: number; apiKey?: string } = {}
): Promise<{ stdout: string; stderr: string; exitCode: number }> {
// Default timeout based on model type
let defaultTimeout = 120000; // 2 minutes default
@@ -123,7 +123,11 @@ export class GrokCliLanguageModel implements LanguageModelV2 {
return new Promise((resolve, reject) => {
const child = spawn('grok', args, {
stdio: 'pipe',
cwd: this.settings.workingDirectory || process.cwd()
cwd: this.settings.workingDirectory || process.cwd(),
env:
options.apiKey === undefined
? process.env
: { ...process.env, GROK_CLI_API_KEY: options.apiKey }
});
let stdout = '';
@@ -295,7 +299,7 @@ export class GrokCliLanguageModel implements LanguageModelV2 {
}
try {
const result = await this.executeGrokCli(args);
const result = await this.executeGrokCli(args, { apiKey });
if (result.exitCode !== 0) {
// Handle authentication errors
@@ -323,11 +327,14 @@ export class GrokCliLanguageModel implements LanguageModelV2 {
let text = response.text || '';
// Extract JSON if in object-json mode
if (
'mode' in options &&
(options as any).mode?.type === 'object-json' &&
text
) {
const isObjectJson = (
o: unknown
): o is { mode: { type: 'object-json' } } =>
!!o &&
typeof o === 'object' &&
'mode' in o &&
(o as any).mode?.type === 'object-json';
if (isObjectJson(options) && text) {
text = extractJson(text);
}

View File

@@ -9,49 +9,49 @@ describe('extractJson', () => {
it('should extract JSON from markdown code blocks', () => {
const text = '```json\n{"name": "test", "value": 42}\n```';
const result = extractJson(text);
expect(result).toBe('{"name": "test", "value": 42}');
expect(JSON.parse(result)).toEqual({ name: 'test', value: 42 });
});
it('should extract JSON from generic code blocks', () => {
const text = '```\n{"name": "test", "value": 42}\n```';
const result = extractJson(text);
expect(result).toBe('{"name": "test", "value": 42}');
expect(JSON.parse(result)).toEqual({ name: 'test', value: 42 });
});
it('should remove JavaScript variable declarations', () => {
const text = 'const result = {"name": "test", "value": 42};';
const result = extractJson(text);
expect(result).toBe('{"name": "test", "value": 42}');
expect(JSON.parse(result)).toEqual({ name: 'test', value: 42 });
});
it('should handle let variable declarations', () => {
const text = 'let data = {"name": "test", "value": 42};';
const result = extractJson(text);
expect(result).toBe('{"name": "test", "value": 42}');
expect(JSON.parse(result)).toEqual({ name: 'test', value: 42 });
});
it('should handle var variable declarations', () => {
const text = 'var config = {"name": "test", "value": 42};';
const result = extractJson(text);
expect(result).toBe('{"name": "test", "value": 42}');
expect(JSON.parse(result)).toEqual({ name: 'test', value: 42 });
});
it('should extract JSON arrays', () => {
const text = '[{"name": "test1"}, {"name": "test2"}]';
const result = extractJson(text);
expect(result).toBe('[{"name": "test1"}, {"name": "test2"}]');
expect(JSON.parse(result)).toEqual([{ name: 'test1' }, { name: 'test2' }]);
});
it('should convert JavaScript object literals to JSON', () => {
const text = "{name: 'test', value: 42}";
const result = extractJson(text);
expect(result).toBe('{"name": "test", "value": 42}');
expect(JSON.parse(result)).toEqual({ name: 'test', value: 42 });
});
it('should return valid JSON as-is', () => {
it('should return valid JSON (canonical formatting)', () => {
const text = '{"name": "test", "value": 42}';
const result = extractJson(text);
expect(result).toBe('{"name": "test", "value": 42}');
expect(JSON.parse(result)).toEqual({ name: 'test', value: 42 });
});
it('should return original text when JSON parsing fails completely', () => {
@@ -76,6 +76,6 @@ describe('extractJson', () => {
it('should handle mixed quotes in object literals', () => {
const text = `{name: "test", value: 'mixed quotes'}`;
const result = extractJson(text);
expect(result).toBe('{"name": "test", "value": "mixed quotes"}');
expect(JSON.parse(result)).toEqual({ name: 'test', value: 'mixed quotes' });
});
});

View File

@@ -121,6 +121,19 @@ describe('createPromptFromMessages', () => {
expect(result).toBe('custom: Custom message');
});
it('should trim whitespace from message content', () => {
const messages = [
{ role: 'user', content: ' Hello with spaces ' },
{ role: 'assistant', content: '\n\nResponse with newlines\n\n' }
];
const result = createPromptFromMessages(messages);
expect(result).toBe(
'User: Hello with spaces\n\nAssistant: Response with newlines'
);
});
});
describe('escapeShellArg', () => {

View File

@@ -81,7 +81,7 @@ export function convertFromGrokCliResponse(responseText: string): {
// Find the last assistant message
const assistantMessage = messages
.filter((msg: any) => msg.role === 'assistant')
.filter((msg) => msg.role === 'assistant')
.pop();
if (assistantMessage && assistantMessage.content) {

View File

@@ -1,7 +1,7 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"module": "ESNext",
"lib": ["ES2022"],
"declaration": true,
"declarationMap": true,
@@ -24,7 +24,7 @@
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "NodeNext",
"moduleResolution": "bundler",
"moduleDetection": "force",
"types": ["node"],
"resolveJsonModule": true,

View File

@@ -21,7 +21,7 @@
},
"devDependencies": {
"dotenv-mono": "^1.5.1",
"typescript": "^5.7.3"
"typescript": "^5.9.2"
},
"dependencies": {
"tsup": "^8.5.0"

View File

@@ -335,7 +335,7 @@ function validateClaudeCodeSettings(settings) {
});
// Define CommandSpecificSchema using the base schema
const CommandSpecificSchema = z.partialRecord(
const CommandSpecificSchema = z.record(
z.enum(AI_COMMAND_NAMES),
BaseSettingsSchema
);

View File

@@ -392,12 +392,29 @@ Ensure the JSON is valid and properly formatted.`;
0
);
let nextId = maxPreservedId + 1;
const normalizedGeneratedSubtasks = processedGeneratedSubtasks.map(
(st) => ({
...st,
id: nextId++
const idMapping = new Map();
const normalizedGeneratedSubtasks = processedGeneratedSubtasks
.map((st) => {
const originalId = st.id;
const newId = nextId++;
idMapping.set(originalId, newId);
return {
...st,
id: newId
};
})
);
.map((st) => ({
...st,
dependencies: (st.dependencies || []).map((dep) => {
if (typeof dep !== 'string' || !dep.startsWith(`${task.id}.`)) {
return dep;
}
const [, siblingIdPart] = dep.split('.');
const originalSiblingId = Number.parseInt(siblingIdPart, 10);
const remappedSiblingId = idMapping.get(originalSiblingId);
return remappedSiblingId ? `${task.id}.${remappedSiblingId}` : dep;
})
}));
// Update task with preserved subtasks + newly generated ones
task.subtasks = [...preservedSubtasks, ...normalizedGeneratedSubtasks];
@@ -406,7 +423,7 @@ Ensure the JSON is valid and properly formatted.`;
updatedTask: task,
regenerated: true,
preserved: preservedSubtasks.length,
generated: processedGeneratedSubtasks.length
generated: normalizedGeneratedSubtasks.length
};
} catch (error) {
log(

View File

@@ -33,33 +33,36 @@ import { ContextGatherer } from '../utils/contextGatherer.js';
import { FuzzyTaskSearch } from '../utils/fuzzyTaskSearch.js';
// Zod schema for post-parsing validation of the updated task object
const updatedTaskSchema = z.object({
id: z.int(),
title: z.string(), // Title should be preserved, but check it exists
description: z.string(),
status: z.string(),
dependencies: z.array(z.union([z.int(), z.string()])),
priority: z.string().nullable().prefault('medium'),
details: z.string().nullable().prefault(''),
testStrategy: z.string().nullable().prefault(''),
subtasks: z
.array(
z.object({
id: z
.int()
.positive()
.describe('Sequential subtask ID starting from 1'),
title: z.string(),
description: z.string(),
status: z.string(),
dependencies: z.array(z.int()).nullable().prefault([]),
details: z.string().nullable().prefault(''),
testStrategy: z.string().nullable().prefault('')
})
)
.nullable()
.prefault([])
}); // Allows parsing even if AI adds extra fields, but validation focuses on schema
const updatedTaskSchema = z
.object({
id: z.number().int(),
title: z.string(), // Title should be preserved, but check it exists
description: z.string(),
status: z.string(),
dependencies: z.array(z.union([z.number().int(), z.string()])),
priority: z.string().nullable().prefault('medium'),
details: z.string().nullable().prefault(''),
testStrategy: z.string().nullable().prefault(''),
subtasks: z
.array(
z.object({
id: z
.number()
.int()
.positive()
.describe('Sequential subtask ID starting from 1'),
title: z.string(),
description: z.string(),
status: z.string(),
dependencies: z.array(z.number().int()).nullable().prefault([]),
details: z.string().nullable().prefault(''),
testStrategy: z.string().nullable().prefault('')
})
)
.nullable()
.prefault([])
})
.strip(); // Enforce the canonical task shape and drop unknown fields
/**
* Parses a single updated task object from AI's text response.

View File

@@ -29,17 +29,19 @@ import { FuzzyTaskSearch } from '../utils/fuzzyTaskSearch.js';
import { flattenTasksWithSubtasks, findProjectRoot } from '../utils.js';
// Zod schema for validating the structure of tasks AFTER parsing
const updatedTaskSchema = z.object({
id: z.int(),
title: z.string(),
description: z.string(),
status: z.string(),
dependencies: z.array(z.union([z.int(), z.string()])),
priority: z.string().nullable(),
details: z.string().nullable(),
testStrategy: z.string().nullable(),
subtasks: z.array(z.any()).nullable() // Keep subtasks flexible for now
}); // Allow potential extra fields during parsing if needed, then validate structure
const updatedTaskSchema = z
.object({
id: z.int(),
title: z.string(),
description: z.string(),
status: z.string(),
dependencies: z.array(z.union([z.int(), z.string()])),
priority: z.string().nullable(),
details: z.string().nullable(),
testStrategy: z.string().nullable(),
subtasks: z.array(z.any()).nullable() // Keep subtasks flexible for now
})
.strip(); // Enforce the canonical task shape and drop unknown fields
// Preprocessing schema that adds defaults before validation
const preprocessTaskSchema = z.preprocess((task) => {

View File

@@ -24,25 +24,18 @@ const { ClaudeCodeProvider } = await import(
);
describe('Claude Code Error Handling', () => {
it('should handle missing Claude Code CLI gracefully', () => {
const provider = new ClaudeCodeProvider();
expect(() => provider.getClient()).toThrow(/Claude Code CLI not available/);
beforeEach(() => {
jest.clearAllMocks();
});
it('should handle CLI errors during client creation', () => {
const provider = new ClaudeCodeProvider();
expect(() => provider.getClient({ commandName: 'test' })).toThrow(
/Claude Code CLI not available/
);
});
it('should provide a helpful CLI-not-available error', () => {
it('should throw a CLI-not-available error (with or without commandName)', () => {
const provider = new ClaudeCodeProvider();
expect(() => provider.getClient()).toThrow(
/Claude Code CLI not available/i
);
expect(() => provider.getClient({ commandName: 'test' })).toThrow(
/Claude Code CLI not available/i
);
});
it('should still support basic provider functionality', () => {
@@ -52,6 +45,7 @@ describe('Claude Code Error Handling', () => {
expect(provider.name).toBe('Claude Code');
expect(provider.getSupportedModels()).toEqual(['sonnet', 'opus']);
expect(provider.isModelSupported('sonnet')).toBe(true);
expect(provider.isModelSupported('haiku')).toBe(false);
expect(provider.isRequiredApiKey()).toBe(false);
expect(() => provider.validateAuth()).not.toThrow();
});