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/inquirer": "^9.0.3",
"@types/node": "^22.10.5", "@types/node": "^22.10.5",
"tsx": "^4.20.4", "tsx": "^4.20.4",
"typescript": "^5.7.3", "typescript": "^5.9.2",
"vitest": "^2.1.8" "vitest": "^2.1.8"
}, },
"engines": { "engines": {

View File

@@ -240,7 +240,7 @@
"check-types": "tsc --noEmit" "check-types": "tsc --noEmit"
}, },
"dependencies": { "dependencies": {
"task-master-ai": "0.27.2" "task-master-ai": "*"
}, },
"devDependencies": { "devDependencies": {
"@dnd-kit/core": "^6.3.1", "@dnd-kit/core": "^6.3.1",
@@ -276,7 +276,7 @@
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"tailwind-merge": "^3.3.1", "tailwind-merge": "^3.3.1",
"tailwindcss": "4.1.11", "tailwindcss": "4.1.11",
"typescript": "^5.7.3" "typescript": "^5.9.2"
}, },
"overrides": { "overrides": {
"glob@<8": "^10.4.5", "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 ### Prerequisites
1. **Install Claude Code CLI** (if not already installed): 1. **Install Claude Code CLI** (if not already installed):
```bash ```bash
# Installation method depends on your system # Installation method depends on your system
# Follow Claude Code documentation for installation # Follow Claude Code documentation for installation
``` ```
2. **Set up OAuth token** using Claude Code CLI: 2. **Set up OAuth token** using Claude Code CLI:
```bash ```bash
claude setup-token claude setup-token
``` ```
@@ -75,6 +77,7 @@ export CLAUDE_CODE_OAUTH_TOKEN="your_oauth_token_here"
``` ```
This is only needed in specific scenarios like: This is only needed in specific scenarios like:
- CI/CD pipelines - CI/CD pipelines
- Docker containers - Docker containers
- When you want to use a different token than the CLI default - 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. **Problem**: Task Master cannot connect to Claude Code CLI.
**Solutions**: **Solutions**:
- Ensure Claude Code CLI is installed and in your PATH - Ensure Claude Code CLI is installed and in your PATH
- Run `claude setup-token` to configure authentication - Run `claude setup-token` to configure authentication
- Verify Claude Code CLI works: `claude --help` - Verify Claude Code CLI works: `claude --help`
@@ -119,6 +123,7 @@ task-master models --setup
**Problem**: Token authentication is failing. **Problem**: Token authentication is failing.
**Solutions**: **Solutions**:
- Re-run `claude setup-token` to refresh your OAuth token - Re-run `claude setup-token` to refresh your OAuth token
- Check if your token has expired - Check if your token has expired
- Verify Claude Code CLI can authenticate: try a simple `claude` command - 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. **Problem**: Specified Claude Code model is not supported.
**Solutions**: **Solutions**:
- Use supported models: `sonnet` or `opus` - Use supported models: `sonnet` or `opus`
- Check model availability: `task-master models --list` - Check model availability: `task-master models --list`
- Verify your Claude Code CLI has access to the requested model - Verify your Claude Code CLI has access to the requested model
@@ -135,18 +141,21 @@ task-master models --setup
### Debug Steps ### Debug Steps
1. **Test Claude Code CLI directly**: 1. **Test Claude Code CLI directly**:
```bash ```bash
claude --help claude --help
# Should show help without errors # Should show help without errors
``` ```
2. **Test authentication**: 2. **Test authentication**:
```bash ```bash
claude setup-token --verify claude setup-token --verify
# Should confirm token is valid # Should confirm token is valid
``` ```
3. **Test Task Master integration**: 3. **Test Task Master integration**:
```bash ```bash
task-master models --test claude-code:sonnet task-master models --test claude-code:sonnet
# Should successfully connect and test the model # 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 1. Install Claude Code CLI in your container
2. Set up authentication via environment variable: 2. Set up authentication via environment variable:
```dockerfile ```dockerfile
ENV CLAUDE_CODE_OAUTH_TOKEN="your_token_here" ENV CLAUDE_CODE_OAUTH_TOKEN="your_token_here"
``` ```
@@ -218,4 +228,4 @@ If you encounter issues:
1. Check the Claude Code CLI documentation 1. Check the Claude Code CLI documentation
2. Verify your authentication setup with `claude setup-token --verify` 2. Verify your authentication setup with `claude setup-token --verify`
3. Review Task Master logs for detailed error messages 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/changelog-github": "^0.5.1",
"@changesets/cli": "^2.28.1", "@changesets/cli": "^2.28.1",
"@manypkg/cli": "^0.25.1", "@manypkg/cli": "^0.25.1",
"@tm/cli": "*",
"@tm/ai-sdk-provider-grok-cli": "*", "@tm/ai-sdk-provider-grok-cli": "*",
"@tm/cli": "*",
"@types/jest": "^29.5.14", "@types/jest": "^29.5.14",
"@types/marked-terminal": "^6.1.1", "@types/marked-terminal": "^6.1.1",
"concurrently": "^9.2.1", "concurrently": "^9.2.1",
@@ -143,7 +143,7 @@
"ts-jest": "^29.4.2", "ts-jest": "^29.4.2",
"tsdown": "^0.15.2", "tsdown": "^0.15.2",
"tsx": "^4.20.4", "tsx": "^4.20.4",
"turbo": "^2.5.6", "turbo": "2.5.6",
"typescript": "^5.7.3" "typescript": "^5.9.2"
} }
} }

View File

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

View File

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

View File

@@ -9,49 +9,49 @@ describe('extractJson', () => {
it('should extract JSON from markdown code blocks', () => { it('should extract JSON from markdown code blocks', () => {
const text = '```json\n{"name": "test", "value": 42}\n```'; const text = '```json\n{"name": "test", "value": 42}\n```';
const result = extractJson(text); 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', () => { it('should extract JSON from generic code blocks', () => {
const text = '```\n{"name": "test", "value": 42}\n```'; const text = '```\n{"name": "test", "value": 42}\n```';
const result = extractJson(text); 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', () => { it('should remove JavaScript variable declarations', () => {
const text = 'const result = {"name": "test", "value": 42};'; const text = 'const result = {"name": "test", "value": 42};';
const result = extractJson(text); 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', () => { it('should handle let variable declarations', () => {
const text = 'let data = {"name": "test", "value": 42};'; const text = 'let data = {"name": "test", "value": 42};';
const result = extractJson(text); 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', () => { it('should handle var variable declarations', () => {
const text = 'var config = {"name": "test", "value": 42};'; const text = 'var config = {"name": "test", "value": 42};';
const result = extractJson(text); 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', () => { it('should extract JSON arrays', () => {
const text = '[{"name": "test1"}, {"name": "test2"}]'; const text = '[{"name": "test1"}, {"name": "test2"}]';
const result = extractJson(text); 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', () => { it('should convert JavaScript object literals to JSON', () => {
const text = "{name: 'test', value: 42}"; const text = "{name: 'test', value: 42}";
const result = extractJson(text); 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 text = '{"name": "test", "value": 42}';
const result = extractJson(text); 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', () => { it('should return original text when JSON parsing fails completely', () => {
@@ -76,6 +76,6 @@ describe('extractJson', () => {
it('should handle mixed quotes in object literals', () => { it('should handle mixed quotes in object literals', () => {
const text = `{name: "test", value: 'mixed quotes'}`; const text = `{name: "test", value: 'mixed quotes'}`;
const result = extractJson(text); 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'); 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', () => { describe('escapeShellArg', () => {

View File

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

View File

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

View File

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

View File

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

View File

@@ -392,12 +392,29 @@ Ensure the JSON is valid and properly formatted.`;
0 0
); );
let nextId = maxPreservedId + 1; let nextId = maxPreservedId + 1;
const normalizedGeneratedSubtasks = processedGeneratedSubtasks.map( const idMapping = new Map();
(st) => ({ const normalizedGeneratedSubtasks = processedGeneratedSubtasks
...st, .map((st) => {
id: nextId++ 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 // Update task with preserved subtasks + newly generated ones
task.subtasks = [...preservedSubtasks, ...normalizedGeneratedSubtasks]; task.subtasks = [...preservedSubtasks, ...normalizedGeneratedSubtasks];
@@ -406,7 +423,7 @@ Ensure the JSON is valid and properly formatted.`;
updatedTask: task, updatedTask: task,
regenerated: true, regenerated: true,
preserved: preservedSubtasks.length, preserved: preservedSubtasks.length,
generated: processedGeneratedSubtasks.length generated: normalizedGeneratedSubtasks.length
}; };
} catch (error) { } catch (error) {
log( log(

View File

@@ -33,33 +33,36 @@ import { ContextGatherer } from '../utils/contextGatherer.js';
import { FuzzyTaskSearch } from '../utils/fuzzyTaskSearch.js'; import { FuzzyTaskSearch } from '../utils/fuzzyTaskSearch.js';
// Zod schema for post-parsing validation of the updated task object // Zod schema for post-parsing validation of the updated task object
const updatedTaskSchema = z.object({ const updatedTaskSchema = z
id: z.int(), .object({
title: z.string(), // Title should be preserved, but check it exists id: z.number().int(),
description: z.string(), title: z.string(), // Title should be preserved, but check it exists
status: z.string(), description: z.string(),
dependencies: z.array(z.union([z.int(), z.string()])), status: z.string(),
priority: z.string().nullable().prefault('medium'), dependencies: z.array(z.union([z.number().int(), z.string()])),
details: z.string().nullable().prefault(''), priority: z.string().nullable().prefault('medium'),
testStrategy: z.string().nullable().prefault(''), details: z.string().nullable().prefault(''),
subtasks: z testStrategy: z.string().nullable().prefault(''),
.array( subtasks: z
z.object({ .array(
id: z z.object({
.int() id: z
.positive() .number()
.describe('Sequential subtask ID starting from 1'), .int()
title: z.string(), .positive()
description: z.string(), .describe('Sequential subtask ID starting from 1'),
status: z.string(), title: z.string(),
dependencies: z.array(z.int()).nullable().prefault([]), description: z.string(),
details: z.string().nullable().prefault(''), status: z.string(),
testStrategy: z.string().nullable().prefault('') dependencies: z.array(z.number().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 .nullable()
.prefault([])
})
.strip(); // Enforce the canonical task shape and drop unknown fields
/** /**
* Parses a single updated task object from AI's text response. * 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'; import { flattenTasksWithSubtasks, findProjectRoot } from '../utils.js';
// Zod schema for validating the structure of tasks AFTER parsing // Zod schema for validating the structure of tasks AFTER parsing
const updatedTaskSchema = z.object({ const updatedTaskSchema = z
id: z.int(), .object({
title: z.string(), id: z.int(),
description: z.string(), title: z.string(),
status: z.string(), description: z.string(),
dependencies: z.array(z.union([z.int(), z.string()])), status: z.string(),
priority: z.string().nullable(), dependencies: z.array(z.union([z.int(), z.string()])),
details: z.string().nullable(), priority: z.string().nullable(),
testStrategy: z.string().nullable(), details: z.string().nullable(),
subtasks: z.array(z.any()).nullable() // Keep subtasks flexible for now testStrategy: z.string().nullable(),
}); // Allow potential extra fields during parsing if needed, then validate structure 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 // Preprocessing schema that adds defaults before validation
const preprocessTaskSchema = z.preprocess((task) => { const preprocessTaskSchema = z.preprocess((task) => {

View File

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