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

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
.map((st) => {
const originalId = st.id;
const newId = nextId++;
idMapping.set(originalId, newId);
return {
...st, ...st,
id: nextId++ 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,12 +33,13 @@ 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({
id: z.number().int(),
title: z.string(), // Title should be preserved, but check it exists title: z.string(), // Title should be preserved, but check it exists
description: z.string(), description: z.string(),
status: z.string(), status: z.string(),
dependencies: z.array(z.union([z.int(), z.string()])), dependencies: z.array(z.union([z.number().int(), z.string()])),
priority: z.string().nullable().prefault('medium'), priority: z.string().nullable().prefault('medium'),
details: z.string().nullable().prefault(''), details: z.string().nullable().prefault(''),
testStrategy: z.string().nullable().prefault(''), testStrategy: z.string().nullable().prefault(''),
@@ -46,20 +47,22 @@ const updatedTaskSchema = z.object({
.array( .array(
z.object({ z.object({
id: z id: z
.number()
.int() .int()
.positive() .positive()
.describe('Sequential subtask ID starting from 1'), .describe('Sequential subtask ID starting from 1'),
title: z.string(), title: z.string(),
description: z.string(), description: z.string(),
status: z.string(), status: z.string(),
dependencies: z.array(z.int()).nullable().prefault([]), dependencies: z.array(z.number().int()).nullable().prefault([]),
details: z.string().nullable().prefault(''), details: z.string().nullable().prefault(''),
testStrategy: z.string().nullable().prefault('') testStrategy: z.string().nullable().prefault('')
}) })
) )
.nullable() .nullable()
.prefault([]) .prefault([])
}); // Allows parsing even if AI adds extra fields, but validation focuses on schema })
.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,7 +29,8 @@ 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
.object({
id: z.int(), id: z.int(),
title: z.string(), title: z.string(),
description: z.string(), description: z.string(),
@@ -39,7 +40,8 @@ const updatedTaskSchema = z.object({
details: z.string().nullable(), details: z.string().nullable(),
testStrategy: z.string().nullable(), testStrategy: z.string().nullable(),
subtasks: z.array(z.any()).nullable() // Keep subtasks flexible for now subtasks: z.array(z.any()).nullable() // Keep subtasks flexible for now
}); // Allow potential extra fields during parsing if needed, then validate structure })
.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();
}); });