chore: apply requested changes
This commit is contained in:
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
5328
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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' });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"dotenv-mono": "^1.5.1",
|
||||
"typescript": "^5.7.3"
|
||||
"typescript": "^5.9.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"tsup": "^8.5.0"
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user