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"
```

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();
});