Compare commits

..

9 Commits

Author SHA1 Message Date
github-actions[bot]
9acb900153 Version Packages (#1303)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Ralph Khreish <35776126+Crunchyman-ralph@users.noreply.github.com>
2025-10-14 11:47:13 +02:00
Ralph Khreish
c4f5d89e72 Merge pull request #1297 from eyaltoledano/next 2025-10-14 10:08:08 +02:00
Ralph Khreish
e308cf4f46 chore: exit pre mode 2025-10-13 22:46:24 +02:00
Ralph Khreish
11b7354010 fix: export url (#1288) 2025-10-13 21:51:19 +02:00
Ralph Khreish
4c1ef2ca94 fix: auth refresh token (#1299)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-10-13 21:50:22 +02:00
Ralph Khreish
663aa2dfe9 chore: fix CI 2025-10-12 17:10:24 +02:00
Ralph Khreish
8f60a0561e chore: add hiring banner 2025-10-12 16:52:58 +02:00
Ralph Khreish
9a22622e9c chore: cleanup changelog and pre exit 2025-10-11 21:21:04 +02:00
github-actions[bot]
8d3c7e4116 chore: rc version bump 2025-10-11 19:09:36 +00:00
41 changed files with 1284 additions and 579 deletions

View File

@@ -1,7 +0,0 @@
---
"task-master-ai": minor
---
Add changelog highlights to auto-update notifications
When the CLI auto-updates to a new version, it now displays a "What's New" section.

View File

@@ -11,6 +11,7 @@
"access": "public",
"baseBranch": "main",
"ignore": [
"docs"
"docs",
"@tm/claude-code-plugin"
]
}

View File

@@ -1,47 +0,0 @@
---
"task-master-ai": minor
---
Add Claude Code plugin with marketplace distribution
This release introduces official Claude Code plugin support, marking the evolution from legacy `.claude` directory copying to a modern plugin-based architecture.
## 🎉 New: Claude Code Plugin
Task Master AI commands and agents are now distributed as a proper Claude Code plugin:
- **49 slash commands** with clean naming (`/task-master-ai:command-name`)
- **3 specialized AI agents** (task-orchestrator, task-executor, task-checker)
- **MCP server integration** for deep Claude Code integration
**Installation:**
```bash
/plugin marketplace add eyaltoledano/claude-task-master
/plugin install taskmaster@taskmaster
```
### The `rules add claude` command no longer copies commands and agents to `.claude/commands/` and `.claude/agents/`. Instead, it now
- Shows plugin installation instructions
- Only manages CLAUDE.md imports for agent instructions
- Directs users to install the official plugin
**Migration for Existing Users:**
If you previously used `rules add claude`:
1. The old commands in `.claude/commands/` will continue to work but won't receive updates
2. Install the plugin for the latest features: `/plugin install taskmaster@taskmaster`
3. remove old `.claude/commands/` and `.claude/agents/` directories
**Why This Change?**
Claude Code plugins provide:
- ✅ Automatic updates when we release new features
- ✅ Better command organization and naming
- ✅ Seamless integration with Claude Code
- ✅ No manual file copying or management
The plugin system is the future of Task Master AI integration with Claude Code!

View File

@@ -1,17 +0,0 @@
---
"task-master-ai": minor
---
Add RPG (Repository Planning Graph) method template for structured PRD creation. The new `example_prd_rpg.txt` template teaches AI agents and developers the RPG methodology through embedded instructions, inline good/bad examples, and XML-style tags for structure. This template enables creation of dependency-aware PRDs that automatically generate topologically-ordered task graphs when parsed with Task Master.
Key features:
- Method-as-template: teaches RPG principles (dual-semantics, explicit dependencies, topological order) while being used
- Inline instructions at decision points guide AI through each section
- Good/bad examples for immediate pattern matching
- Flexible plain-text format with XML-style tags for parseability
- Critical dependency-graph section ensures correct task ordering
- Automatic inclusion during `task-master init`
- Comprehensive documentation at [docs.task-master.dev/capabilities/rpg-method](https://docs.task-master.dev/capabilities/rpg-method)
- Tool recommendations for code-context-aware PRD creation (Claude Code, Cursor, Gemini CLI, Codex/Grok)
The RPG template complements the existing `example_prd.txt` and provides a more structured approach for complex projects requiring clear module boundaries and dependency chains.

View File

@@ -1,7 +0,0 @@
---
"task-master-ai": patch
---
Fix cross-level task dependencies not being saved
Fixes an issue where adding dependencies between subtasks and top-level tasks (e.g., `task-master add-dependency --id=2.2 --depends-on=11`) would report success but fail to persist the changes. Dependencies can now be created in both directions between any task levels.

View File

@@ -1,16 +0,0 @@
---
"task-master-ai": minor
---
Enhance `expand_all` to intelligently use complexity analysis recommendations when expanding tasks.
The expand-all operation now automatically leverages recommendations from `analyze-complexity` to determine optimal subtask counts for each task, resulting in more accurate and context-aware task breakdowns.
Key improvements:
- Automatic integration with complexity analysis reports
- Tag-aware complexity report path resolution
- Intelligent subtask count determination based on task complexity
- Falls back to defaults when complexity analysis is unavailable
- Enhanced logging for better visibility into expansion decisions
When you run `task-master expand --all` after `task-master analyze-complexity`, Task Master now uses the recommended subtask counts from the complexity analysis instead of applying uniform defaults, ensuring each task is broken down according to its actual complexity.

View File

@@ -1,5 +1,175 @@
# task-master-ai
## 0.29.0
### Minor Changes
- [#1286](https://github.com/eyaltoledano/claude-task-master/pull/1286) [`f12a16d`](https://github.com/eyaltoledano/claude-task-master/commit/f12a16d09649f62148515f11f616157c7d0bd2d5) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Add changelog highlights to auto-update notifications
When the CLI auto-updates to a new version, it now displays a "What's New" section.
- [#1293](https://github.com/eyaltoledano/claude-task-master/pull/1293) [`3010b90`](https://github.com/eyaltoledano/claude-task-master/commit/3010b90d98f3a7d8636caa92fc33d6ee69d4bed0) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Add Claude Code plugin with marketplace distribution
This release introduces official Claude Code plugin support, marking the evolution from legacy `.claude` directory copying to a modern plugin-based architecture.
## 🎉 New: Claude Code Plugin
Task Master AI commands and agents are now distributed as a proper Claude Code plugin:
- **49 slash commands** with clean naming (`/taskmaster:command-name`)
- **3 specialized AI agents** (task-orchestrator, task-executor, task-checker)
- **MCP server integration** for deep Claude Code integration
**Installation:**
```bash
/plugin marketplace add eyaltoledano/claude-task-master
/plugin install taskmaster@taskmaster
```
### The `rules add claude` command no longer copies commands and agents to `.claude/commands/` and `.claude/agents/`. Instead, it now
- Shows plugin installation instructions
- Only manages CLAUDE.md imports for agent instructions
- Directs users to install the official plugin
**Migration for Existing Users:**
If you previously used `rules add claude`:
1. The old commands in `.claude/commands/` will continue to work but won't receive updates
2. Install the plugin for the latest features: `/plugin install taskmaster@taskmaster`
3. remove old `.claude/commands/` and `.claude/agents/` directories
**Why This Change?**
Claude Code plugins provide:
- ✅ Automatic updates when we release new features
- ✅ Better command organization and naming
- ✅ Seamless integration with Claude Code
- ✅ No manual file copying or management
The plugin system is the future of Task Master AI integration with Claude Code!
- [#1285](https://github.com/eyaltoledano/claude-task-master/pull/1285) [`2a910a4`](https://github.com/eyaltoledano/claude-task-master/commit/2a910a40bac375f9f61d797bf55597303d556b48) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Add RPG (Repository Planning Graph) method template for structured PRD creation. The new `example_prd_rpg.txt` template teaches AI agents and developers the RPG methodology through embedded instructions, inline good/bad examples, and XML-style tags for structure. This template enables creation of dependency-aware PRDs that automatically generate topologically-ordered task graphs when parsed with Task Master.
Key features:
- Method-as-template: teaches RPG principles (dual-semantics, explicit dependencies, topological order) while being used
- Inline instructions at decision points guide AI through each section
- Good/bad examples for immediate pattern matching
- Flexible plain-text format with XML-style tags for parseability
- Critical dependency-graph section ensures correct task ordering
- Automatic inclusion during `task-master init`
- Comprehensive documentation at [docs.task-master.dev/capabilities/rpg-method](https://docs.task-master.dev/capabilities/rpg-method)
- Tool recommendations for code-context-aware PRD creation (Claude Code, Cursor, Gemini CLI, Codex/Grok)
The RPG template complements the existing `example_prd.txt` and provides a more structured approach for complex projects requiring clear module boundaries and dependency chains.
- [#1287](https://github.com/eyaltoledano/claude-task-master/pull/1287) [`90e6bdc`](https://github.com/eyaltoledano/claude-task-master/commit/90e6bdcf1c59f65ad27fcdfe3b13b9dca7e77654) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Enhance `expand_all` to intelligently use complexity analysis recommendations when expanding tasks.
The expand-all operation now automatically leverages recommendations from `analyze-complexity` to determine optimal subtask counts for each task, resulting in more accurate and context-aware task breakdowns.
Key improvements:
- Automatic integration with complexity analysis reports
- Tag-aware complexity report path resolution
- Intelligent subtask count determination based on task complexity
- Falls back to defaults when complexity analysis is unavailable
- Enhanced logging for better visibility into expansion decisions
When you run `task-master expand --all` after `task-master analyze-complexity`, Task Master now uses the recommended subtask counts from the complexity analysis instead of applying uniform defaults, ensuring each task is broken down according to its actual complexity.
### Patch Changes
- [#1191](https://github.com/eyaltoledano/claude-task-master/pull/1191) [`aaf903f`](https://github.com/eyaltoledano/claude-task-master/commit/aaf903ff2f606c779a22e9a4b240ab57b3683815) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Fix cross-level task dependencies not being saved
Fixes an issue where adding dependencies between subtasks and top-level tasks (e.g., `task-master add-dependency --id=2.2 --depends-on=11`) would report success but fail to persist the changes. Dependencies can now be created in both directions between any task levels.
- [#1299](https://github.com/eyaltoledano/claude-task-master/pull/1299) [`4c1ef2c`](https://github.com/eyaltoledano/claude-task-master/commit/4c1ef2ca94411c53bcd2a78ec710b06c500236dd) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Improve refresh token when authenticating
## 0.29.0-rc.1
### Patch Changes
- [#1299](https://github.com/eyaltoledano/claude-task-master/pull/1299) [`a6c5152`](https://github.com/eyaltoledano/claude-task-master/commit/a6c5152f20edd8717cf1aea34e7c178b1261aa99) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Improve refresh token when authenticating
## 0.29.0-rc.0
### Minor Changes
- [#1286](https://github.com/eyaltoledano/claude-task-master/pull/1286) [`f12a16d`](https://github.com/eyaltoledano/claude-task-master/commit/f12a16d09649f62148515f11f616157c7d0bd2d5) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Add changelog highlights to auto-update notifications
When the CLI auto-updates to a new version, it now displays a "What's New" section.
- [#1293](https://github.com/eyaltoledano/claude-task-master/pull/1293) [`3010b90`](https://github.com/eyaltoledano/claude-task-master/commit/3010b90d98f3a7d8636caa92fc33d6ee69d4bed0) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Add Claude Code plugin with marketplace distribution
This release introduces official Claude Code plugin support, marking the evolution from legacy `.claude` directory copying to a modern plugin-based architecture.
## 🎉 New: Claude Code Plugin
Task Master AI commands and agents are now distributed as a proper Claude Code plugin:
- **49 slash commands** with clean naming (`/task-master-ai:command-name`)
- **3 specialized AI agents** (task-orchestrator, task-executor, task-checker)
- **MCP server integration** for deep Claude Code integration
**Installation:**
```bash
/plugin marketplace add eyaltoledano/claude-task-master
/plugin install taskmaster@taskmaster
```
### The `rules add claude` command no longer copies commands and agents to `.claude/commands/` and `.claude/agents/`. Instead, it now
- Shows plugin installation instructions
- Only manages CLAUDE.md imports for agent instructions
- Directs users to install the official plugin
**Migration for Existing Users:**
If you previously used `rules add claude`:
1. The old commands in `.claude/commands/` will continue to work but won't receive updates
2. Install the plugin for the latest features: `/plugin install taskmaster@taskmaster`
3. remove old `.claude/commands/` and `.claude/agents/` directories
**Why This Change?**
Claude Code plugins provide:
- ✅ Automatic updates when we release new features
- ✅ Better command organization and naming
- ✅ Seamless integration with Claude Code
- ✅ No manual file copying or management
The plugin system is the future of Task Master AI integration with Claude Code!
- [#1285](https://github.com/eyaltoledano/claude-task-master/pull/1285) [`2a910a4`](https://github.com/eyaltoledano/claude-task-master/commit/2a910a40bac375f9f61d797bf55597303d556b48) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Add RPG (Repository Planning Graph) method template for structured PRD creation. The new `example_prd_rpg.txt` template teaches AI agents and developers the RPG methodology through embedded instructions, inline good/bad examples, and XML-style tags for structure. This template enables creation of dependency-aware PRDs that automatically generate topologically-ordered task graphs when parsed with Task Master.
Key features:
- Method-as-template: teaches RPG principles (dual-semantics, explicit dependencies, topological order) while being used
- Inline instructions at decision points guide AI through each section
- Good/bad examples for immediate pattern matching
- Flexible plain-text format with XML-style tags for parseability
- Critical dependency-graph section ensures correct task ordering
- Automatic inclusion during `task-master init`
- Comprehensive documentation at [docs.task-master.dev/capabilities/rpg-method](https://docs.task-master.dev/capabilities/rpg-method)
- Tool recommendations for code-context-aware PRD creation (Claude Code, Cursor, Gemini CLI, Codex/Grok)
The RPG template complements the existing `example_prd.txt` and provides a more structured approach for complex projects requiring clear module boundaries and dependency chains.
- [#1287](https://github.com/eyaltoledano/claude-task-master/pull/1287) [`90e6bdc`](https://github.com/eyaltoledano/claude-task-master/commit/90e6bdcf1c59f65ad27fcdfe3b13b9dca7e77654) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Enhance `expand_all` to intelligently use complexity analysis recommendations when expanding tasks.
The expand-all operation now automatically leverages recommendations from `analyze-complexity` to determine optimal subtask counts for each task, resulting in more accurate and context-aware task breakdowns.
Key improvements:
- Automatic integration with complexity analysis reports
- Tag-aware complexity report path resolution
- Intelligent subtask count determination based on task complexity
- Falls back to defaults when complexity analysis is unavailable
- Enhanced logging for better visibility into expansion decisions
When you run `task-master expand --all` after `task-master analyze-complexity`, Task Master now uses the recommended subtask counts from the complexity analysis instead of applying uniform defaults, ensuring each task is broken down according to its actual complexity.
### Patch Changes
- [#1191](https://github.com/eyaltoledano/claude-task-master/pull/1191) [`aaf903f`](https://github.com/eyaltoledano/claude-task-master/commit/aaf903ff2f606c779a22e9a4b240ab57b3683815) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Fix cross-level task dependencies not being saved
Fixes an issue where adding dependencies between subtasks and top-level tasks (e.g., `task-master add-dependency --id=2.2 --depends-on=11`) would report success but fail to persist the changes. Dependencies can now be created in both directions between any task levels.
## 0.28.0
### Minor Changes

View File

@@ -310,6 +310,12 @@ cd claude-task-master
node scripts/init.js
```
## Join Our Team
<a href="https://tryhamster.com" target="_blank">
<img src="./images/hamster-hiring.png" alt="Join Hamster's founding team" />
</a>
## Contributors
<a href="https://github.com/eyaltoledano/claude-task-master/graphs/contributors">

View File

@@ -11,6 +11,13 @@
### Patch Changes
- Updated dependencies []:
- @tm/core@null
## null
### Patch Changes
- Updated dependencies []:
- @tm/core@null

View File

@@ -143,7 +143,7 @@ export class AuthCommand extends Command {
*/
private async executeStatus(): Promise<void> {
try {
const result = this.displayStatus();
const result = await this.displayStatus();
this.setLastResult(result);
} catch (error: any) {
this.handleError(error);
@@ -171,8 +171,8 @@ export class AuthCommand extends Command {
/**
* Display authentication status
*/
private displayStatus(): AuthResult {
const credentials = this.authManager.getCredentials();
private async displayStatus(): Promise<AuthResult> {
const credentials = await this.authManager.getCredentials();
console.log(chalk.cyan('\n🔐 Authentication Status\n'));
@@ -187,19 +187,29 @@ export class AuthCommand extends Command {
if (credentials.expiresAt) {
const expiresAt = new Date(credentials.expiresAt);
const now = new Date();
const hoursRemaining = Math.floor(
(expiresAt.getTime() - now.getTime()) / (1000 * 60 * 60)
);
const timeRemaining = expiresAt.getTime() - now.getTime();
const hoursRemaining = Math.floor(timeRemaining / (1000 * 60 * 60));
const minutesRemaining = Math.floor(timeRemaining / (1000 * 60));
if (hoursRemaining > 0) {
console.log(
chalk.gray(
` Expires: ${expiresAt.toLocaleString()} (${hoursRemaining} hours remaining)`
)
);
if (timeRemaining > 0) {
// Token is still valid
if (hoursRemaining > 0) {
console.log(
chalk.gray(
` Expires at: ${expiresAt.toLocaleString()} (${hoursRemaining} hours remaining)`
)
);
} else {
console.log(
chalk.gray(
` Expires at: ${expiresAt.toLocaleString()} (${minutesRemaining} minutes remaining)`
)
);
}
} else {
// Token has expired
console.log(
chalk.yellow(` Token expired at: ${expiresAt.toLocaleString()}`)
chalk.yellow(` Expired at: ${expiresAt.toLocaleString()}`)
);
}
} else {
@@ -315,7 +325,7 @@ export class AuthCommand extends Command {
]);
if (!continueAuth) {
const credentials = this.authManager.getCredentials();
const credentials = await this.authManager.getCredentials();
ui.displaySuccess('Using existing authentication');
if (credentials) {
@@ -480,7 +490,7 @@ export class AuthCommand extends Command {
/**
* Get current credentials (for programmatic usage)
*/
getCredentials(): AuthCredentials | null {
getCredentials(): Promise<AuthCredentials | null> {
return this.authManager.getCredentials();
}

View File

@@ -115,7 +115,7 @@ export class ContextCommand extends Command {
*/
private async executeShow(): Promise<void> {
try {
const result = this.displayContext();
const result = await this.displayContext();
this.setLastResult(result);
} catch (error: any) {
this.handleError(error);
@@ -126,7 +126,7 @@ export class ContextCommand extends Command {
/**
* Display current context
*/
private displayContext(): ContextResult {
private async displayContext(): Promise<ContextResult> {
// Check authentication first
if (!this.authManager.isAuthenticated()) {
console.log(chalk.yellow('✗ Not authenticated'));
@@ -139,7 +139,7 @@ export class ContextCommand extends Command {
};
}
const context = this.authManager.getContext();
const context = await this.authManager.getContext();
console.log(chalk.cyan('\n🌍 Workspace Context\n'));
@@ -263,7 +263,7 @@ export class ContextCommand extends Command {
return {
success: true,
action: 'select-org',
context: this.authManager.getContext() || undefined,
context: (await this.authManager.getContext()) || undefined,
message: `Selected organization: ${selectedOrg.name}`
};
} catch (error) {
@@ -284,7 +284,7 @@ export class ContextCommand extends Command {
}
// Check if org is selected
const context = this.authManager.getContext();
const context = await this.authManager.getContext();
if (!context?.orgId) {
ui.displayError(
'No organization selected. Run "tm context org" first.'
@@ -353,7 +353,7 @@ export class ContextCommand extends Command {
return {
success: true,
action: 'select-brief',
context: this.authManager.getContext() || undefined,
context: (await this.authManager.getContext()) || undefined,
message: `Selected brief: ${selectedBrief.name}`
};
} else {
@@ -368,7 +368,7 @@ export class ContextCommand extends Command {
return {
success: true,
action: 'select-brief',
context: this.authManager.getContext() || undefined,
context: (await this.authManager.getContext()) || undefined,
message: 'Cleared brief selection'
};
}
@@ -508,7 +508,7 @@ export class ContextCommand extends Command {
this.setLastResult({
success: true,
action: 'set',
context: this.authManager.getContext() || undefined,
context: (await this.authManager.getContext()) || undefined,
message: 'Context set from brief'
});
} catch (error: any) {
@@ -631,7 +631,7 @@ export class ContextCommand extends Command {
return {
success: true,
action: 'set',
context: this.authManager.getContext() || undefined,
context: (await this.authManager.getContext()) || undefined,
message: 'Context updated'
};
} catch (error) {
@@ -682,7 +682,7 @@ export class ContextCommand extends Command {
/**
* Get current context (for programmatic usage)
*/
getContext(): UserContext | null {
getContext(): Promise<UserContext | null> {
return this.authManager.getContext();
}

View File

@@ -103,7 +103,7 @@ export class ExportCommand extends Command {
await this.initializeServices();
// Get current context
const context = this.authManager.getContext();
const context = await this.authManager.getContext();
// Determine org and brief IDs
let orgId = options?.org || context?.orgId;

View File

@@ -1,5 +1,7 @@
# docs
## 0.0.6
## 0.0.5
## 0.0.4

View File

@@ -33,8 +33,6 @@
]
},
"getting-started/api-keys",
"getting-started/claude-code-plugin",
"getting-started/migration-plugin",
"getting-started/faq",
"getting-started/contribute"
]

View File

@@ -1,129 +0,0 @@
# Claude Code Plugin Integration
Task Master AI now offers official Claude Code plugin support, providing seamless integration with 49 specialized commands and 3 AI agents.
## Installation
### Quick Installation
Install the plugin directly from the Task Master marketplace:
```bash
/plugin marketplace add eyaltoledano/claude-task-master
/plugin install taskmaster@taskmaster
```
### What You Get
- **49 Slash Commands**: All Task Master commands accessible via `/task-master-ai:` prefix
- **3 Specialized Agents**: task-orchestrator, task-executor, and task-checker
- **MCP Integration**: Deep integration with Claude Code's MCP system
- **Automatic Updates**: Plugin updates automatically with new releases
## Quick Start with Plugin
After installation, initialize your project:
```bash
/task-master-ai:init-project
/task-master-ai:parse-prd
/task-master-ai:next-task
```
## Command Reference
All Task Master commands are available with the `/task-master-ai:` prefix:
### Core Workflow
- `/task-master-ai:init-project` - Initialize Task Master in current project
- `/task-master-ai:parse-prd` - Generate tasks from PRD document
- `/task-master-ai:next-task` - Get next available task
- `/task-master-ai:show-task` - View detailed task information
### Task Management
- `/task-master-ai:add-task` - Add new task with AI assistance
- `/task-master-ai:expand-task` - Break task into subtasks
- `/task-master-ai:to-done` - Mark task complete
- `/task-master-ai:list-tasks` - Show all tasks with status
### Analysis & Planning
- `/task-master-ai:analyze-complexity` - Analyze task complexity
- `/task-master-ai:complexity-report` - View complexity analysis
- `/task-master-ai:expand-all-tasks` - Expand all eligible tasks
## AI Agents
The plugin includes three specialized agents for different workflow needs:
### Task Orchestrator
High-level project coordination and strategic planning.
### Task Executor
Hands-on implementation and code generation.
### Task Checker
Quality assurance and validation of completed work.
## Migration from Legacy Setup
<Warning>
If you previously used `rules add claude`, those commands will continue working but won't receive updates.
</Warning>
### Migration Steps
1. **Install the plugin**: `/plugin install taskmaster@taskmaster`
2. **Remove old files** (optional):
```bash
rm -rf .claude/commands/tm/
rm -rf .claude/agents/task-*
```
3. **Update workflows** to use new command names with `/task-master-ai:` prefix
### Why Migrate?
- ✅ **Automatic updates** - Get new features without manual copying
- ✅ **Better organization** - Clean command naming and structure
- ✅ **Seamless integration** - Native Claude Code plugin experience
- ✅ **No file management** - No need to manually maintain command files
## Team Configuration
Organizations can auto-install the plugin for team members:
```json
{
"extraKnownMarketplaces": {
"task-master": {
"source": {
"source": "github",
"repo": "eyaltoledano/claude-task-master"
}
}
},
"enabledPlugins": {
"taskmaster": {
"marketplace": "taskmaster"
}
}
}
```
Add this to `.claude/settings.json` in your repository root.
## Troubleshooting
### Plugin Not Found
Ensure you've added the marketplace first:
```bash
/plugin marketplace add eyaltoledano/claude-task-master
```
### Commands Not Working
Verify the plugin is installed and enabled:
```bash
/plugin list
```
### MCP Integration Issues
Check that your MCP configuration includes the Task Master server as outlined in the [MCP documentation](/capabilities/mcp).

View File

@@ -1,144 +0,0 @@
# Migrating to Claude Code Plugin
<Warning>
If you previously used `task-master init --rules claude`, this guide will help you migrate to the new plugin system.
</Warning>
## What Changed?
Task Master AI has evolved from copying files to `.claude/commands/` and `.claude/agents/` directories to a modern plugin-based architecture that provides:
- ✅ **Automatic updates** when new features are released
- ✅ **Better command organization** with clean `/task-master-ai:` prefixes
- ✅ **Seamless Claude Code integration** using native plugin system
- ✅ **No manual file management** - no more copying or updating files
## Migration Steps
### 1. Install the Plugin
First, install the official Task Master plugin:
```bash
/plugin marketplace add eyaltoledano/claude-task-master
/plugin install taskmaster@taskmaster
```
### 2. Verify Plugin Installation
Check that the plugin is working:
```bash
/task-master-ai:help
```
You should see the full list of available commands.
### 3. Remove Old Files (Optional)
<Warning>
Your existing Task Master project files (`.taskmaster/` folder) are NOT affected and will continue working normally.
</Warning>
The old command files in `.claude/` are now redundant. You can safely remove them:
```bash
# Remove old command files
rm -rf .claude/commands/tm/
# Remove old agent files
rm -rf .claude/agents/task-*
```
### 4. Update Your Workflows
Update any saved workflows or documentation to use the new command names:
#### Old Commands (still work but won't update)
```bash
/tm:init
/tm:parse-prd
/tm:next
```
#### New Commands (get automatic updates)
```bash
/task-master-ai:init-project
/task-master-ai:parse-prd
/task-master-ai:next-task
```
## Command Name Changes
Most commands have the same name but with the new prefix:
| Old Format | New Format |
|------------|------------|
| `/tm:init` | `/task-master-ai:init-project` |
| `/tm:parse-prd` | `/task-master-ai:parse-prd` |
| `/tm:next` | `/task-master-ai:next-task` |
| `/tm:show` | `/task-master-ai:show-task` |
| `/tm:add-task` | `/task-master-ai:add-task` |
| `/tm:expand` | `/task-master-ai:expand-task` |
| `/tm:to-done` | `/task-master-ai:to-done` |
## What Happens to MCP?
<Note>
The MCP server integration remains fully functional and is recommended alongside the plugin for the complete Task Master experience.
</Note>
The plugin provides slash commands and agents, while the MCP server provides deep integration tools. For the best experience, keep both:
1. **Plugin**: For slash commands and AI agents
2. **MCP Server**: For advanced tool integration
## Troubleshooting Migration
### Old Commands Still Showing
If you're still seeing old commands after removing the files:
1. Restart Claude Code completely
2. Clear command cache if available in your editor
### Plugin Commands Not Working
1. Verify plugin installation: `/plugin list`
2. Check marketplace is added: `/plugin marketplace list`
3. Reinstall if needed: `/plugin uninstall taskmaster && /plugin install taskmaster@taskmaster`
### MCP Tools Not Working
If your MCP integration breaks during migration:
1. Verify your `.mcp.json` configuration is intact
2. Restart your editor to reconnect MCP servers
3. Check API keys are still configured correctly
## Benefits of Migration
### Automatic Updates
- New commands and features arrive automatically
- No need to run `rules add claude` again
- Always get the latest Task Master capabilities
### Better Organization
- Clean command naming with consistent prefixes
- Better integration with Claude Code's plugin system
- Reduced local file clutter
### Enhanced Functionality
- 49 specialized commands (vs. previous limited set)
- 3 AI agents for different workflow needs
- Native Claude Code plugin experience
## Need Help?
If you encounter issues during migration:
1. Check the [FAQ](/getting-started/faq) for common issues
2. Join our [Discord community](https://discord.gg/fWJkU7rf) for support
3. File an issue on [GitHub](https://github.com/eyaltoledano/claude-task-master/issues)
The migration should be seamless, but we're here to help if you run into any problems!

View File

@@ -3,38 +3,9 @@ title: Installation
sidebarTitle: "Installation"
---
Now that you have Node.js and your first API Key, you are ready to begin installing Task Master in one of three ways.
Now that you have Node.js and your first API Key, you are ready to begin installing Task Master in one of three ways.
## Recommended: Claude Code Plugin
<Note>**New!** Task Master is now available as an official Claude Code plugin with automatic updates and seamless integration.</Note>
### Quick Install for Claude Code
```bash
/plugin marketplace add eyaltoledano/claude-task-master
/plugin install taskmaster@taskmaster
```
After installation, initialize your project:
```bash
/task-master-ai:init-project
```
**Benefits:**
- ✅ Automatic updates when new features are released
- ✅ 49 specialized slash commands
- ✅ 3 AI agents for different workflows
- ✅ No manual file management required
[Learn more about the Claude Code plugin →](/getting-started/claude-code-plugin)
---
## Alternative Installation Methods
<Note>Cursor Users Can Use the One Click MCP Install Below</Note>
<Note>Cursor Users Can Use the One Click Install Below</Note>
<Accordion title="Quick Install for Cursor 1.0+ (One-Click)">
<a href="cursor://anysphere.cursor-deeplink/mcp/install?name=task-master-ai&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIi0tcGFja2FnZT10YXNrLW1hc3Rlci1haSIsInRhc2stbWFzdGVyLWFpIl0sImVudiI6eyJBTlRIUk9QSUNfQVBJX0tFWSI6IllPVVJfQU5USFJPUElDX0FQSV9LRVlfSEVSRSIsIlBFUlBMRVhJVFlfQVBJX0tFWSI6IllPVVJfUEVSUExFWElUWV9BUElfS0VZX0hFUkUiLCJPUEVOQUlfQVBJX0tFWSI6IllPVVJfT1BFTkFJX0tFWV9IRVJFIiwiR09PR0xFX0FQSV9LRVkiOiJZT1VSX0dPT0dMRV9LRVlfSEVSRSIsIk1JU1RSQUxfQVBJX0tFWSI6IllPVVJfTUlTVFJBTF9LRVlfSEVSRSIsIk9QRU5ST1VURVJfQVBJX0tFWSI6IllPVVJfT1BFTlJPVVRFUl9LRVlfSEVSRSIsIlhBSV9BUElfS0VZIjoiWU9VUl9YQUlfS0VZX0hFUkUiLCJBWlVSRV9PUEVOQUJFX0FQSV9LRVkiOiJZT1VSX0FaVVJFX0tFWV9IRVJFIiwiT0xMQU1BX0FQSV9LRVkiOiJZT1VSX09MTEFNQV9BUElfS0VZX0hFUkUifX0%3D">

View File

@@ -1,5 +1,5 @@
<Tip>
Welcome to v1 of the Task Master Docs. **New!** Task Master is now available as an official Claude Code plugin with 49 commands and 3 AI agents.
Welcome to v1 of the Task Master Docs. Expect weekly updates as we expand and refine each section.
</Tip>
We've organized the docs into three sections depending on your experience level and goals:

View File

@@ -1,6 +1,6 @@
{
"name": "docs",
"version": "0.0.5",
"version": "0.0.6",
"private": true,
"description": "Task Master documentation powered by Mintlify",
"scripts": {

View File

@@ -3,26 +3,4 @@ title: "What's New"
sidebarTitle: "What's New"
---
## 🎉 Claude Code Plugin Now Available (Latest)
Task Master AI has evolved with official Claude Code plugin support!
### What's New
- **49 specialized slash commands** with clean `/task-master-ai:` naming
- **3 AI agents** for orchestration, execution, and checking
- **Automatic updates** - no more manual file copying
- **Seamless integration** with Claude Code's native plugin system
### Quick Install
```bash
/plugin marketplace add eyaltoledano/claude-task-master
/plugin install taskmaster@taskmaster
```
[Learn more →](/getting-started/claude-code-plugin) | [Migration guide →](/getting-started/migration-plugin)
---
## Previous Releases
An easy way to see the latest releases

View File

@@ -1,5 +1,14 @@
# Change Log
## 0.25.6
## 0.25.6-rc.0
### Patch Changes
- Updated dependencies [[`f12a16d`](https://github.com/eyaltoledano/claude-task-master/commit/f12a16d09649f62148515f11f616157c7d0bd2d5), [`3010b90`](https://github.com/eyaltoledano/claude-task-master/commit/3010b90d98f3a7d8636caa92fc33d6ee69d4bed0), [`2a910a4`](https://github.com/eyaltoledano/claude-task-master/commit/2a910a40bac375f9f61d797bf55597303d556b48), [`aaf903f`](https://github.com/eyaltoledano/claude-task-master/commit/aaf903ff2f606c779a22e9a4b240ab57b3683815), [`90e6bdc`](https://github.com/eyaltoledano/claude-task-master/commit/90e6bdcf1c59f65ad27fcdfe3b13b9dca7e77654)]:
- task-master-ai@0.29.0-rc.0
## 0.25.5
### Patch Changes

View File

@@ -3,7 +3,7 @@
"private": true,
"displayName": "TaskMaster",
"description": "A visual Kanban board interface for TaskMaster projects in VS Code",
"version": "0.25.5",
"version": "0.25.6",
"publisher": "Hamster",
"icon": "assets/icon.png",
"engines": {
@@ -239,9 +239,6 @@
"watch:css": "npx @tailwindcss/cli -i ./src/webview/index.css -o ./dist/index.css --watch",
"check-types": "tsc --noEmit"
},
"dependencies": {
"task-master-ai": "*"
},
"devDependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0",
@@ -277,7 +274,8 @@
"tailwind-merge": "^3.3.1",
"tailwindcss": "4.1.11",
"typescript": "^5.9.2",
"@tm/core": "*"
"@tm/core": "*",
"task-master-ai": "*"
},
"overrides": {
"glob@<8": "^10.4.5",

BIN
images/hamster-hiring.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

File diff suppressed because one or more lines are too long

109
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "task-master-ai",
"version": "0.28.0",
"version": "0.29.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "task-master-ai",
"version": "0.28.0",
"version": "0.29.0",
"license": "MIT WITH Commons-Clause",
"workspaces": [
"apps/*",
@@ -125,16 +125,13 @@
}
},
"apps/docs": {
"version": "0.0.5",
"version": "0.0.6",
"devDependencies": {
"mintlify": "^4.2.111"
}
},
"apps/extension": {
"version": "0.25.5",
"dependencies": {
"task-master-ai": "*"
},
"version": "0.25.6",
"devDependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0",
@@ -170,6 +167,7 @@
"react-dom": "^19.0.0",
"tailwind-merge": "^3.3.1",
"tailwindcss": "4.1.11",
"task-master-ai": "*",
"typescript": "^5.9.2"
},
"engines": {
@@ -178,6 +176,7 @@
},
"apps/extension/node_modules/@ai-sdk/amazon-bedrock": {
"version": "2.2.12",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@ai-sdk/provider": "1.1.3",
@@ -195,6 +194,7 @@
},
"apps/extension/node_modules/@ai-sdk/anthropic": {
"version": "1.2.12",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@ai-sdk/provider": "1.1.3",
@@ -209,6 +209,7 @@
},
"apps/extension/node_modules/@ai-sdk/azure": {
"version": "1.3.25",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@ai-sdk/openai": "1.3.24",
@@ -224,6 +225,7 @@
},
"apps/extension/node_modules/@ai-sdk/google": {
"version": "1.2.22",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@ai-sdk/provider": "1.1.3",
@@ -238,6 +240,7 @@
},
"apps/extension/node_modules/@ai-sdk/google-vertex": {
"version": "2.2.27",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@ai-sdk/anthropic": "1.2.12",
@@ -255,6 +258,7 @@
},
"apps/extension/node_modules/@ai-sdk/groq": {
"version": "1.2.9",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@ai-sdk/provider": "1.1.3",
@@ -269,6 +273,7 @@
},
"apps/extension/node_modules/@ai-sdk/mistral": {
"version": "1.2.8",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@ai-sdk/provider": "1.1.3",
@@ -283,6 +288,7 @@
},
"apps/extension/node_modules/@ai-sdk/openai": {
"version": "1.3.24",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@ai-sdk/provider": "1.1.3",
@@ -297,6 +303,7 @@
},
"apps/extension/node_modules/@ai-sdk/openai-compatible": {
"version": "0.2.16",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@ai-sdk/provider": "1.1.3",
@@ -311,6 +318,7 @@
},
"apps/extension/node_modules/@ai-sdk/perplexity": {
"version": "1.1.9",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@ai-sdk/provider": "1.1.3",
@@ -325,6 +333,7 @@
},
"apps/extension/node_modules/@ai-sdk/provider": {
"version": "1.1.3",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"json-schema": "^0.4.0"
@@ -335,6 +344,7 @@
},
"apps/extension/node_modules/@ai-sdk/provider-utils": {
"version": "2.2.8",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@ai-sdk/provider": "1.1.3",
@@ -350,6 +360,7 @@
},
"apps/extension/node_modules/@ai-sdk/react": {
"version": "1.2.12",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@ai-sdk/provider-utils": "2.2.8",
@@ -372,6 +383,7 @@
},
"apps/extension/node_modules/@ai-sdk/ui-utils": {
"version": "1.2.11",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@ai-sdk/provider": "1.1.3",
@@ -387,6 +399,7 @@
},
"apps/extension/node_modules/@ai-sdk/xai": {
"version": "1.2.18",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@ai-sdk/openai-compatible": "0.2.16",
@@ -402,6 +415,7 @@
},
"apps/extension/node_modules/@openrouter/ai-sdk-provider": {
"version": "0.4.6",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@ai-sdk/provider": "1.0.9",
@@ -416,6 +430,7 @@
},
"apps/extension/node_modules/@openrouter/ai-sdk-provider/node_modules/@ai-sdk/provider": {
"version": "1.0.9",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"json-schema": "^0.4.0"
@@ -426,6 +441,7 @@
},
"apps/extension/node_modules/@openrouter/ai-sdk-provider/node_modules/@ai-sdk/provider-utils": {
"version": "2.1.10",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@ai-sdk/provider": "1.0.9",
@@ -447,6 +463,7 @@
},
"apps/extension/node_modules/ai": {
"version": "4.3.19",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@ai-sdk/provider": "1.1.3",
@@ -471,6 +488,7 @@
},
"apps/extension/node_modules/ai-sdk-provider-gemini-cli": {
"version": "0.1.3",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -504,6 +522,7 @@
},
"apps/extension/node_modules/ollama-ai-provider": {
"version": "1.2.0",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@ai-sdk/provider": "^1.0.0",
@@ -524,6 +543,7 @@
},
"apps/extension/node_modules/openai": {
"version": "4.104.0",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@types/node": "^18.11.18",
@@ -552,6 +572,7 @@
},
"apps/extension/node_modules/openai/node_modules/@types/node": {
"version": "18.19.127",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~5.26.4"
@@ -559,10 +580,12 @@
},
"apps/extension/node_modules/openai/node_modules/undici-types": {
"version": "5.26.5",
"dev": true,
"license": "MIT"
},
"apps/extension/node_modules/task-master-ai": {
"version": "0.27.1",
"dev": true,
"license": "MIT WITH Commons-Clause",
"workspaces": [
"apps/*",
@@ -634,6 +657,7 @@
},
"apps/extension/node_modules/zod": {
"version": "3.25.76",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
@@ -641,6 +665,7 @@
},
"apps/extension/node_modules/zod-to-json-schema": {
"version": "3.24.6",
"dev": true,
"license": "ISC",
"peerDependencies": {
"zod": "^3.24.1"
@@ -929,6 +954,7 @@
},
"node_modules/@anthropic-ai/sdk": {
"version": "0.39.0",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "^18.11.18",
@@ -942,6 +968,7 @@
},
"node_modules/@anthropic-ai/sdk/node_modules/@types/node": {
"version": "18.19.127",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~5.26.4"
@@ -949,6 +976,7 @@
},
"node_modules/@anthropic-ai/sdk/node_modules/undici-types": {
"version": "5.26.5",
"dev": true,
"license": "MIT"
},
"node_modules/@ark/schema": {
@@ -8408,6 +8436,7 @@
},
"node_modules/@types/diff-match-patch": {
"version": "1.0.36",
"dev": true,
"license": "MIT"
},
"node_modules/@types/es-aggregate-error": {
@@ -8578,6 +8607,7 @@
},
"node_modules/@types/node-fetch": {
"version": "2.6.13",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*",
@@ -9023,6 +9053,7 @@
},
"node_modules/abort-controller": {
"version": "3.0.0",
"dev": true,
"license": "MIT",
"dependencies": {
"event-target-shim": "^5.0.0"
@@ -9084,6 +9115,7 @@
},
"node_modules/agentkeepalive": {
"version": "4.6.0",
"dev": true,
"license": "MIT",
"dependencies": {
"humanize-ms": "^1.2.1"
@@ -9666,6 +9698,7 @@
},
"node_modules/asynckit": {
"version": "0.4.0",
"dev": true,
"license": "MIT"
},
"node_modules/auto-bind": {
@@ -11443,6 +11476,7 @@
},
"node_modules/combined-stream": {
"version": "1.0.8",
"dev": true,
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
@@ -12081,6 +12115,7 @@
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.4.0"
@@ -12103,6 +12138,7 @@
},
"node_modules/dequal": {
"version": "2.0.3",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
@@ -12222,6 +12258,7 @@
},
"node_modules/diff-match-patch": {
"version": "1.0.5",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/diff-sequences": {
@@ -12721,6 +12758,7 @@
},
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
@@ -13009,6 +13047,7 @@
},
"node_modules/event-target-shim": {
"version": "5.0.1",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
@@ -14047,6 +14086,7 @@
},
"node_modules/form-data": {
"version": "4.0.4",
"dev": true,
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
@@ -14061,6 +14101,7 @@
},
"node_modules/form-data-encoder": {
"version": "1.7.2",
"dev": true,
"license": "MIT"
},
"node_modules/format": {
@@ -14072,6 +14113,7 @@
},
"node_modules/formdata-node": {
"version": "4.4.1",
"dev": true,
"license": "MIT",
"dependencies": {
"node-domexception": "1.0.0",
@@ -14732,6 +14774,7 @@
},
"node_modules/has-tostringtag": {
"version": "1.0.2",
"dev": true,
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
@@ -15306,6 +15349,7 @@
},
"node_modules/humanize-ms": {
"version": "1.2.1",
"dev": true,
"license": "MIT",
"dependencies": {
"ms": "^2.0.0"
@@ -18092,6 +18136,7 @@
},
"node_modules/jsondiffpatch": {
"version": "0.6.0",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/diff-match-patch": "^1.0.36",
@@ -20273,6 +20318,7 @@
},
"node_modules/nanoid": {
"version": "3.3.11",
"devOptional": true,
"funding": [
{
"type": "github",
@@ -20381,6 +20427,7 @@
},
"node_modules/node-domexception": {
"version": "1.0.0",
"dev": true,
"funding": [
{
"type": "github",
@@ -21245,6 +21292,7 @@
},
"node_modules/partial-json": {
"version": "0.1.7",
"dev": true,
"license": "MIT"
},
"node_modules/patch-console": {
@@ -22017,6 +22065,7 @@
},
"node_modules/react": {
"version": "19.1.1",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -23143,6 +23192,7 @@
},
"node_modules/secure-json-parse": {
"version": "2.7.0",
"dev": true,
"license": "BSD-3-Clause"
},
"node_modules/selderee": {
@@ -24190,6 +24240,26 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/strip-literal": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz",
"integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==",
"dev": true,
"license": "MIT",
"dependencies": {
"js-tokens": "^9.0.1"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/strip-literal/node_modules/js-tokens": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz",
"integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==",
"dev": true,
"license": "MIT"
},
"node_modules/strnum": {
"version": "2.1.1",
"funding": [
@@ -24367,6 +24437,7 @@
},
"node_modules/swr": {
"version": "2.3.6",
"dev": true,
"license": "MIT",
"dependencies": {
"dequal": "^2.0.3",
@@ -24559,6 +24630,7 @@
},
"node_modules/throttleit": {
"version": "2.1.0",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
@@ -25661,6 +25733,7 @@
},
"node_modules/use-sync-external-store": {
"version": "1.5.0",
"dev": true,
"license": "MIT",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
@@ -26069,6 +26142,7 @@
},
"node_modules/web-streams-polyfill": {
"version": "4.0.0-beta.3",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 14"
@@ -27062,22 +27136,8 @@
},
"packages/claude-code-plugin": {
"name": "@tm/claude-code-plugin",
"license": "MIT WITH Commons-Clause",
"devDependencies": {
"@types/node": "^20.0.0",
"tsx": "^4.20.4",
"typescript": "^5.9.2"
}
},
"packages/claude-code-plugin/node_modules/@types/node": {
"version": "20.19.20",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.20.tgz",
"integrity": "sha512-2Q7WS25j4pS1cS8yw3d6buNCVJukOTeQ39bAnwR6sOJbaxvyCGebzTMypDFN82CxBLnl+lSWVdCCWbRY6y9yZQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
"version": "0.0.2",
"license": "MIT WITH Commons-Clause"
},
"packages/tm-core": {
"name": "@tm/core",
@@ -27089,6 +27149,7 @@
"devDependencies": {
"@types/node": "^22.10.5",
"@vitest/coverage-v8": "^3.2.4",
"strip-literal": "3.1.0",
"typescript": "^5.9.2",
"vitest": "^3.2.4"
}
@@ -27398,6 +27459,8 @@
},
"packages/tm-core/node_modules/vitest": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz",
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
"dev": true,
"license": "MIT",
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "task-master-ai",
"version": "0.28.0",
"version": "0.29.0",
"description": "A task management system for ambitious AI-driven development that doesn't overwhelm and confuse Cursor.",
"main": "index.js",
"type": "module",

View File

@@ -1,3 +1,5 @@
# @tm/ai-sdk-provider-grok-cli
## null
## null

View File

@@ -4,4 +4,6 @@
## null
## null
## 1.0.1

View File

@@ -0,0 +1,3 @@
# @tm/claude-code-plugin
## 0.0.2

View File

@@ -1,6 +1,6 @@
{
"name": "@tm/claude-code-plugin",
"version": "0.0.1",
"version": "0.0.2",
"description": "Task Master AI plugin for Claude Code - AI-powered task management with commands, agents, and MCP integration",
"type": "module",
"private": true,

View File

@@ -4,6 +4,8 @@
## null
## null
## 0.26.1
All notable changes to the @task-master/tm-core package will be documented in this file.

View File

@@ -37,7 +37,8 @@
"@types/node": "^22.10.5",
"@vitest/coverage-v8": "^3.2.4",
"typescript": "^5.9.2",
"vitest": "^3.2.4"
"vitest": "^3.2.4",
"strip-literal": "3.1.0"
},
"files": ["src", "README.md", "CHANGELOG.md"],
"keywords": ["task-management", "typescript", "ai", "prd", "parser"],

View File

@@ -21,11 +21,16 @@ const CredentialStoreSpy = vi.fn();
vi.mock('./credential-store.js', () => {
return {
CredentialStore: class {
static getInstance(config?: any) {
return new (this as any)(config);
}
static resetInstance() {
// Mock reset instance method
}
constructor(config: any) {
CredentialStoreSpy(config);
this.getCredentials = vi.fn(() => null);
}
getCredentials() {
getCredentials(_options?: any) {
return null;
}
saveCredentials() {}
@@ -85,7 +90,7 @@ describe('AuthManager Singleton', () => {
expect(instance1).toBe(instance2);
});
it('should use config on first call', () => {
it('should use config on first call', async () => {
const config = {
baseUrl: 'https://test.auth.com',
configDir: '/test/config',
@@ -101,7 +106,7 @@ describe('AuthManager Singleton', () => {
// Verify the config is passed to internal components through observable behavior
// getCredentials would look in the configured file path
const credentials = instance.getCredentials();
const credentials = await instance.getCredentials();
expect(credentials).toBeNull(); // File doesn't exist, but config was propagated correctly
});

View File

@@ -29,6 +29,8 @@ export class AuthManager {
private oauthService: OAuthService;
private supabaseClient: SupabaseAuthClient;
private organizationService?: OrganizationService;
private logger = getLogger('AuthManager');
private refreshPromise: Promise<AuthCredentials> | null = null;
private constructor(config?: Partial<AuthConfig>) {
this.credentialStore = CredentialStore.getInstance(config);
@@ -36,7 +38,10 @@ export class AuthManager {
this.oauthService = new OAuthService(this.credentialStore, config);
// Initialize Supabase client with session restoration
this.initializeSupabaseSession();
// Fire-and-forget with catch handler to prevent unhandled rejections
this.initializeSupabaseSession().catch(() => {
// Errors are already logged in initializeSupabaseSession
});
}
/**
@@ -78,8 +83,60 @@ export class AuthManager {
/**
* Get stored authentication credentials
* Automatically refreshes the token if expired
*/
getCredentials(): AuthCredentials | null {
async getCredentials(): Promise<AuthCredentials | null> {
const credentials = this.credentialStore.getCredentials({
allowExpired: true
});
if (!credentials) {
return null;
}
// Check if credentials are expired (with 30-second clock skew buffer)
const CLOCK_SKEW_MS = 30_000;
const isExpired = credentials.expiresAt
? new Date(credentials.expiresAt).getTime() <= Date.now() + CLOCK_SKEW_MS
: false;
// If expired and we have a refresh token, attempt refresh
if (isExpired && credentials.refreshToken) {
// Return existing refresh promise if one is in progress
if (this.refreshPromise) {
try {
return await this.refreshPromise;
} catch {
return null;
}
}
try {
this.logger.info('Token expired, attempting automatic refresh...');
this.refreshPromise = this.refreshToken();
const result = await this.refreshPromise;
return result;
} catch (error) {
this.logger.warn('Automatic token refresh failed:', error);
return null;
} finally {
this.refreshPromise = null;
}
}
// Return null if expired and no refresh token
if (isExpired) {
return null;
}
return credentials;
}
/**
* Get stored authentication credentials (synchronous version)
* Does not attempt automatic refresh
*/
getCredentialsSync(): AuthCredentials | null {
return this.credentialStore.getCredentials();
}
@@ -171,8 +228,8 @@ export class AuthManager {
/**
* Get the current user context (org/brief selection)
*/
getContext(): UserContext | null {
const credentials = this.getCredentials();
async getContext(): Promise<UserContext | null> {
const credentials = await this.getCredentials();
return credentials?.selectedContext || null;
}
@@ -180,7 +237,7 @@ export class AuthManager {
* Update the user context (org/brief selection)
*/
async updateContext(context: Partial<UserContext>): Promise<void> {
const credentials = this.getCredentials();
const credentials = await this.getCredentials();
if (!credentials) {
throw new AuthenticationError('Not authenticated', 'NOT_AUTHENTICATED');
}
@@ -206,7 +263,7 @@ export class AuthManager {
* Clear the user context
*/
async clearContext(): Promise<void> {
const credentials = this.getCredentials();
const credentials = await this.getCredentials();
if (!credentials) {
throw new AuthenticationError('Not authenticated', 'NOT_AUTHENTICATED');
}
@@ -223,7 +280,7 @@ export class AuthManager {
private async getOrganizationService(): Promise<OrganizationService> {
if (!this.organizationService) {
// First check if we have credentials with a token
const credentials = this.getCredentials();
const credentials = await this.getCredentials();
if (!credentials || !credentials.token) {
throw new AuthenticationError('Not authenticated', 'NOT_AUTHENTICATED');
}

View File

@@ -0,0 +1,289 @@
/**
* @fileoverview Unit tests for CredentialStore token expiration handling
*/
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import fs from 'fs';
import path from 'path';
import os from 'os';
import { CredentialStore } from './credential-store';
import type { AuthCredentials } from './types';
describe('CredentialStore - Token Expiration', () => {
let credentialStore: CredentialStore;
let tmpDir: string;
let authFile: string;
beforeEach(() => {
// Create temp directory for test credentials
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tm-cred-test-'));
authFile = path.join(tmpDir, 'auth.json');
// Create instance with test config
CredentialStore.resetInstance();
credentialStore = CredentialStore.getInstance({
configDir: tmpDir,
configFile: authFile
});
});
afterEach(() => {
// Clean up
try {
if (fs.existsSync(tmpDir)) {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
} catch {
// Ignore cleanup errors
}
CredentialStore.resetInstance();
});
describe('Expiration Detection', () => {
it('should return null for expired token', () => {
const expiredCredentials: AuthCredentials = {
token: 'expired-token',
refreshToken: 'refresh-token',
userId: 'test-user',
email: 'test@example.com',
expiresAt: new Date(Date.now() - 60000).toISOString(), // 1 minute ago
savedAt: new Date().toISOString()
};
credentialStore.saveCredentials(expiredCredentials);
const retrieved = credentialStore.getCredentials();
expect(retrieved).toBeNull();
});
it('should return credentials for valid token', () => {
const validCredentials: AuthCredentials = {
token: 'valid-token',
refreshToken: 'refresh-token',
userId: 'test-user',
email: 'test@example.com',
expiresAt: new Date(Date.now() + 3600000).toISOString(), // 1 hour from now
savedAt: new Date().toISOString()
};
credentialStore.saveCredentials(validCredentials);
const retrieved = credentialStore.getCredentials();
expect(retrieved).not.toBeNull();
expect(retrieved?.token).toBe('valid-token');
});
it('should return expired token when allowExpired is true', () => {
const expiredCredentials: AuthCredentials = {
token: 'expired-token',
refreshToken: 'refresh-token',
userId: 'test-user',
email: 'test@example.com',
expiresAt: new Date(Date.now() - 60000).toISOString(),
savedAt: new Date().toISOString()
};
credentialStore.saveCredentials(expiredCredentials);
const retrieved = credentialStore.getCredentials({ allowExpired: true });
expect(retrieved).not.toBeNull();
expect(retrieved?.token).toBe('expired-token');
});
});
describe('Clock Skew Tolerance', () => {
it('should reject token expiring within 30-second buffer', () => {
// Token expires in 15 seconds (within 30-second buffer)
const almostExpiredCredentials: AuthCredentials = {
token: 'almost-expired-token',
refreshToken: 'refresh-token',
userId: 'test-user',
email: 'test@example.com',
expiresAt: new Date(Date.now() + 15000).toISOString(),
savedAt: new Date().toISOString()
};
credentialStore.saveCredentials(almostExpiredCredentials);
const retrieved = credentialStore.getCredentials();
expect(retrieved).toBeNull();
});
it('should accept token expiring outside 30-second buffer', () => {
// Token expires in 60 seconds (outside 30-second buffer)
const validCredentials: AuthCredentials = {
token: 'valid-token',
refreshToken: 'refresh-token',
userId: 'test-user',
email: 'test@example.com',
expiresAt: new Date(Date.now() + 60000).toISOString(),
savedAt: new Date().toISOString()
};
credentialStore.saveCredentials(validCredentials);
const retrieved = credentialStore.getCredentials();
expect(retrieved).not.toBeNull();
expect(retrieved?.token).toBe('valid-token');
});
});
describe('Timestamp Format Handling', () => {
it('should handle ISO string timestamps', () => {
const credentials: AuthCredentials = {
token: 'test-token',
refreshToken: 'refresh-token',
userId: 'test-user',
email: 'test@example.com',
expiresAt: new Date(Date.now() + 3600000).toISOString(),
savedAt: new Date().toISOString()
};
credentialStore.saveCredentials(credentials);
const retrieved = credentialStore.getCredentials();
expect(retrieved).not.toBeNull();
expect(typeof retrieved?.expiresAt).toBe('number'); // Normalized to number
});
it('should handle numeric timestamps', () => {
const credentials: AuthCredentials = {
token: 'test-token',
refreshToken: 'refresh-token',
userId: 'test-user',
email: 'test@example.com',
expiresAt: Date.now() + 3600000,
savedAt: new Date().toISOString()
};
credentialStore.saveCredentials(credentials);
const retrieved = credentialStore.getCredentials();
expect(retrieved).not.toBeNull();
expect(typeof retrieved?.expiresAt).toBe('number');
});
it('should return null for invalid timestamp format', () => {
// Manually write invalid timestamp to file
const invalidCredentials = {
token: 'test-token',
refreshToken: 'refresh-token',
userId: 'test-user',
email: 'test@example.com',
expiresAt: 'invalid-date',
savedAt: new Date().toISOString()
};
fs.writeFileSync(authFile, JSON.stringify(invalidCredentials), {
mode: 0o600
});
const retrieved = credentialStore.getCredentials();
expect(retrieved).toBeNull();
});
it('should return null for missing expiresAt', () => {
const credentialsWithoutExpiry = {
token: 'test-token',
refreshToken: 'refresh-token',
userId: 'test-user',
email: 'test@example.com',
savedAt: new Date().toISOString()
};
fs.writeFileSync(authFile, JSON.stringify(credentialsWithoutExpiry), {
mode: 0o600
});
const retrieved = credentialStore.getCredentials();
expect(retrieved).toBeNull();
});
});
describe('Storage Persistence', () => {
it('should persist expiresAt as ISO string', () => {
const expiryTime = Date.now() + 3600000;
const credentials: AuthCredentials = {
token: 'test-token',
refreshToken: 'refresh-token',
userId: 'test-user',
email: 'test@example.com',
expiresAt: expiryTime,
savedAt: new Date().toISOString()
};
credentialStore.saveCredentials(credentials);
// Read raw file to verify format
const fileContent = fs.readFileSync(authFile, 'utf-8');
const parsed = JSON.parse(fileContent);
// Should be stored as ISO string
expect(typeof parsed.expiresAt).toBe('string');
expect(parsed.expiresAt).toMatch(/^\d{4}-\d{2}-\d{2}T/); // ISO format
});
it('should normalize timestamp on retrieval', () => {
const credentials: AuthCredentials = {
token: 'test-token',
refreshToken: 'refresh-token',
userId: 'test-user',
email: 'test@example.com',
expiresAt: new Date(Date.now() + 3600000).toISOString(),
savedAt: new Date().toISOString()
};
credentialStore.saveCredentials(credentials);
const retrieved = credentialStore.getCredentials();
// Should be normalized to number for runtime use
expect(typeof retrieved?.expiresAt).toBe('number');
});
});
describe('hasValidCredentials', () => {
it('should return false for expired credentials', () => {
const expiredCredentials: AuthCredentials = {
token: 'expired-token',
refreshToken: 'refresh-token',
userId: 'test-user',
email: 'test@example.com',
expiresAt: new Date(Date.now() - 60000).toISOString(),
savedAt: new Date().toISOString()
};
credentialStore.saveCredentials(expiredCredentials);
expect(credentialStore.hasValidCredentials()).toBe(false);
});
it('should return true for valid credentials', () => {
const validCredentials: AuthCredentials = {
token: 'valid-token',
refreshToken: 'refresh-token',
userId: 'test-user',
email: 'test@example.com',
expiresAt: new Date(Date.now() + 3600000).toISOString(),
savedAt: new Date().toISOString()
};
credentialStore.saveCredentials(validCredentials);
expect(credentialStore.hasValidCredentials()).toBe(true);
});
it('should return false when no credentials exist', () => {
expect(credentialStore.hasValidCredentials()).toBe(false);
});
});
});

View File

@@ -47,8 +47,8 @@ export class SupabaseTaskRepository {
* Gets the current brief ID from auth context
* @throws {Error} If no brief is selected
*/
private getBriefIdOrThrow(): string {
const context = this.authManager.getContext();
private async getBriefIdOrThrow(): Promise<string> {
const context = await this.authManager.getContext();
if (!context?.briefId) {
throw new Error(
'No brief selected. Please select a brief first using: tm context brief'
@@ -61,7 +61,7 @@ export class SupabaseTaskRepository {
_projectId?: string,
options?: LoadTasksOptions
): Promise<Task[]> {
const briefId = this.getBriefIdOrThrow();
const briefId = await this.getBriefIdOrThrow();
// Build query with filters
let query = this.supabase
@@ -114,7 +114,7 @@ export class SupabaseTaskRepository {
}
async getTask(_projectId: string, taskId: string): Promise<Task | null> {
const briefId = this.getBriefIdOrThrow();
const briefId = await this.getBriefIdOrThrow();
const { data, error } = await this.supabase
.from('tasks')
@@ -157,7 +157,7 @@ export class SupabaseTaskRepository {
taskId: string,
updates: Partial<Task>
): Promise<Task> {
const briefId = this.getBriefIdOrThrow();
const briefId = await this.getBriefIdOrThrow();
// Validate updates using Zod schema
try {

View File

@@ -105,7 +105,7 @@ export class ExportService {
}
// Get current context
const context = this.authManager.getContext();
const context = await this.authManager.getContext();
// Determine org and brief IDs
let orgId = options.orgId || context?.orgId;
@@ -232,7 +232,7 @@ export class ExportService {
hasBrief: boolean;
context: UserContext | null;
}> {
const context = this.authManager.getContext();
const context = await this.authManager.getContext();
return {
hasOrg: !!context?.orgId,
@@ -362,7 +362,7 @@ export class ExportService {
if (useAPIEndpoint) {
// Use the new bulk import API endpoint
const apiUrl = `${process.env.TM_PUBLIC_BASE_DOMAIN}/ai/api/v1/briefs/${briefId}/tasks/bulk`;
const apiUrl = `${process.env.TM_PUBLIC_BASE_DOMAIN}/ai/api/v1/briefs/${briefId}/tasks`;
// Transform tasks to flat structure for API
const flatTasks = this.transformTasksForBulkImport(tasks);
@@ -370,16 +370,16 @@ export class ExportService {
// Prepare request body
const requestBody = {
source: 'task-master-cli',
accountId: orgId,
options: {
dryRun: false,
stopOnError: false
},
accountId: orgId,
tasks: flatTasks
};
// Get auth token
const credentials = this.authManager.getCredentials();
const credentials = await this.authManager.getCredentials();
if (!credentials || !credentials.token) {
throw new Error('Not authenticated');
}

View File

@@ -119,7 +119,7 @@ export class ApiStorage implements IStorage {
private async loadTagsIntoCache(): Promise<void> {
try {
const authManager = AuthManager.getInstance();
const context = authManager.getContext();
const context = await authManager.getContext();
// If we have a selected brief, create a virtual "tag" for it
if (context?.briefId) {
@@ -152,7 +152,7 @@ export class ApiStorage implements IStorage {
try {
const authManager = AuthManager.getInstance();
const context = authManager.getContext();
const context = await authManager.getContext();
// If no brief is selected in context, throw an error
if (!context?.briefId) {
@@ -318,7 +318,7 @@ export class ApiStorage implements IStorage {
try {
const authManager = AuthManager.getInstance();
const context = authManager.getContext();
const context = await authManager.getContext();
// In our API-based system, we only have one "tag" at a time - the current brief
if (context?.briefId) {

View File

@@ -72,8 +72,8 @@ export class StorageFactory {
{ storageType: 'api', missing }
);
}
// Use auth token from AuthManager
const credentials = authManager.getCredentials();
// Use auth token from AuthManager (synchronous - no auto-refresh here)
const credentials = authManager.getCredentialsSync();
if (credentials) {
// Merge with existing storage config, ensuring required fields
const nextStorage: StorageSettings = {
@@ -103,7 +103,7 @@ export class StorageFactory {
// Then check if authenticated via AuthManager
if (authManager.isAuthenticated()) {
const credentials = authManager.getCredentials();
const credentials = authManager.getCredentialsSync();
if (credentials) {
// Configure API storage with auth credentials
const nextStorage: StorageSettings = {

View File

@@ -0,0 +1,176 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import fs from 'fs';
import os from 'os';
import path from 'path';
import type { Session } from '@supabase/supabase-js';
import { AuthManager } from '../../src/auth/auth-manager';
import { CredentialStore } from '../../src/auth/credential-store';
import type { AuthCredentials } from '../../src/auth/types';
describe('AuthManager Token Refresh', () => {
let authManager: AuthManager;
let credentialStore: CredentialStore;
let tmpDir: string;
let authFile: string;
beforeEach(() => {
// Reset singletons
AuthManager.resetInstance();
CredentialStore.resetInstance();
// Create temporary directory for test isolation
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tm-auth-refresh-'));
authFile = path.join(tmpDir, 'auth.json');
// Initialize AuthManager with test config (this will create CredentialStore internally)
authManager = AuthManager.getInstance({
configDir: tmpDir,
configFile: authFile
});
// Get the CredentialStore instance that AuthManager created
credentialStore = CredentialStore.getInstance();
credentialStore.clearCredentials();
});
afterEach(() => {
// Clean up
try {
credentialStore.clearCredentials();
} catch {
// Ignore cleanup errors
}
AuthManager.resetInstance();
CredentialStore.resetInstance();
vi.restoreAllMocks();
// Remove temporary directory
if (tmpDir && fs.existsSync(tmpDir)) {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
});
it('should not make concurrent refresh requests', async () => {
// Set up expired credentials with refresh token
const expiredCredentials: AuthCredentials = {
token: 'expired_access_token',
refreshToken: 'valid_refresh_token',
userId: 'test-user-id',
email: 'test@example.com',
expiresAt: new Date(Date.now() - 1000).toISOString(), // Expired 1 second ago
savedAt: new Date().toISOString()
};
credentialStore.saveCredentials(expiredCredentials);
// Mock the refreshToken method to track calls
const refreshTokenSpy = vi.spyOn(authManager as any, 'refreshToken');
const mockSession: Session = {
access_token: 'new_access_token',
refresh_token: 'new_refresh_token',
expires_at: Math.floor(Date.now() / 1000) + 3600,
user: {
id: 'test-user-id',
email: 'test@example.com',
app_metadata: {},
user_metadata: {},
aud: 'authenticated',
created_at: new Date().toISOString()
}
};
refreshTokenSpy.mockResolvedValue({
token: mockSession.access_token,
refreshToken: mockSession.refresh_token,
userId: mockSession.user.id,
email: mockSession.user.email,
expiresAt: new Date(mockSession.expires_at! * 1000).toISOString(),
savedAt: new Date().toISOString()
});
// Make multiple concurrent calls to getCredentials
const promises = [
authManager.getCredentials(),
authManager.getCredentials(),
authManager.getCredentials()
];
const results = await Promise.all(promises);
// Verify all calls returned the same new credentials
expect(results[0]?.token).toBe('new_access_token');
expect(results[1]?.token).toBe('new_access_token');
expect(results[2]?.token).toBe('new_access_token');
// Verify refreshToken was only called once, not three times
expect(refreshTokenSpy).toHaveBeenCalledTimes(1);
});
it('should return valid credentials without attempting refresh', async () => {
// Set up valid (non-expired) credentials
const validCredentials: AuthCredentials = {
token: 'valid_access_token',
refreshToken: 'valid_refresh_token',
userId: 'test-user-id',
email: 'test@example.com',
expiresAt: new Date(Date.now() + 3600000).toISOString(), // Expires in 1 hour
savedAt: new Date().toISOString()
};
credentialStore.saveCredentials(validCredentials);
// Spy on refreshToken to ensure it's not called
const refreshTokenSpy = vi.spyOn(authManager as any, 'refreshToken');
const credentials = await authManager.getCredentials();
expect(credentials?.token).toBe('valid_access_token');
expect(refreshTokenSpy).not.toHaveBeenCalled();
});
it('should return null if credentials are expired with no refresh token', async () => {
// Set up expired credentials WITHOUT refresh token
const expiredCredentials: AuthCredentials = {
token: 'expired_access_token',
refreshToken: undefined,
userId: 'test-user-id',
email: 'test@example.com',
expiresAt: new Date(Date.now() - 1000).toISOString(), // Expired 1 second ago
savedAt: new Date().toISOString()
};
credentialStore.saveCredentials(expiredCredentials);
const credentials = await authManager.getCredentials();
expect(credentials).toBeNull();
});
it('should return null if no credentials exist', async () => {
const credentials = await authManager.getCredentials();
expect(credentials).toBeNull();
});
it('should handle refresh failures gracefully', async () => {
// Set up expired credentials with refresh token
const expiredCredentials: AuthCredentials = {
token: 'expired_access_token',
refreshToken: 'invalid_refresh_token',
userId: 'test-user-id',
email: 'test@example.com',
expiresAt: new Date(Date.now() - 1000).toISOString(),
savedAt: new Date().toISOString()
};
credentialStore.saveCredentials(expiredCredentials);
// Mock refreshToken to throw an error
const refreshTokenSpy = vi.spyOn(authManager as any, 'refreshToken');
refreshTokenSpy.mockRejectedValue(new Error('Refresh failed'));
const credentials = await authManager.getCredentials();
expect(credentials).toBeNull();
expect(refreshTokenSpy).toHaveBeenCalledTimes(1);
});
});

View File

@@ -0,0 +1,392 @@
/**
* @fileoverview Integration tests for JWT token auto-refresh functionality
*
* These tests verify that expired tokens are automatically refreshed
* when making API calls through AuthManager.
*/
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import fs from 'fs';
import os from 'os';
import path from 'path';
import type { Session } from '@supabase/supabase-js';
import { AuthManager } from '../../src/auth/auth-manager';
import { CredentialStore } from '../../src/auth/credential-store';
import type { AuthCredentials } from '../../src/auth/types';
describe('AuthManager - Token Auto-Refresh Integration', () => {
let authManager: AuthManager;
let credentialStore: CredentialStore;
let tmpDir: string;
let authFile: string;
// Mock Supabase session that will be returned on refresh
const mockRefreshedSession: Session = {
access_token: 'new-access-token-xyz',
refresh_token: 'new-refresh-token-xyz',
token_type: 'bearer',
expires_at: Math.floor(Date.now() / 1000) + 3600, // 1 hour from now
expires_in: 3600,
user: {
id: 'test-user-id',
email: 'test@example.com',
aud: 'authenticated',
role: 'authenticated',
app_metadata: {},
user_metadata: {},
created_at: new Date().toISOString()
}
};
beforeEach(() => {
// Reset singletons
AuthManager.resetInstance();
CredentialStore.resetInstance();
// Create temporary directory for test isolation
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tm-auth-integration-'));
authFile = path.join(tmpDir, 'auth.json');
// Initialize AuthManager with test config (this will create CredentialStore internally)
authManager = AuthManager.getInstance({
configDir: tmpDir,
configFile: authFile
});
// Get the CredentialStore instance that AuthManager created
credentialStore = CredentialStore.getInstance();
credentialStore.clearCredentials();
});
afterEach(() => {
// Clean up
try {
credentialStore.clearCredentials();
} catch {
// Ignore cleanup errors
}
AuthManager.resetInstance();
CredentialStore.resetInstance();
vi.restoreAllMocks();
// Remove temporary directory
if (tmpDir && fs.existsSync(tmpDir)) {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
});
describe('Expired Token Detection', () => {
it('should detect expired token', async () => {
// Set up expired credentials
const expiredCredentials: AuthCredentials = {
token: 'expired-token',
refreshToken: 'valid-refresh-token',
userId: 'test-user-id',
email: 'test@example.com',
expiresAt: new Date(Date.now() - 60000).toISOString(), // 1 minute ago
savedAt: new Date().toISOString()
};
credentialStore.saveCredentials(expiredCredentials);
authManager = AuthManager.getInstance();
// Mock the Supabase refreshSession to return new tokens
const mockRefreshSession = vi
.fn()
.mockResolvedValue(mockRefreshedSession);
vi.spyOn(
authManager['supabaseClient'],
'refreshSession'
).mockImplementation(mockRefreshSession);
// Get credentials should trigger refresh
const credentials = await authManager.getCredentials();
expect(mockRefreshSession).toHaveBeenCalledTimes(1);
expect(credentials).not.toBeNull();
expect(credentials?.token).toBe('new-access-token-xyz');
});
it('should not refresh valid token', async () => {
// Set up valid credentials
const validCredentials: AuthCredentials = {
token: 'valid-token',
refreshToken: 'valid-refresh-token',
userId: 'test-user-id',
email: 'test@example.com',
expiresAt: new Date(Date.now() + 3600000).toISOString(), // 1 hour from now
savedAt: new Date().toISOString()
};
credentialStore.saveCredentials(validCredentials);
authManager = AuthManager.getInstance();
// Mock refresh to ensure it's not called
const mockRefreshSession = vi.fn();
vi.spyOn(
authManager['supabaseClient'],
'refreshSession'
).mockImplementation(mockRefreshSession);
const credentials = await authManager.getCredentials();
expect(mockRefreshSession).not.toHaveBeenCalled();
expect(credentials?.token).toBe('valid-token');
});
});
describe('Token Refresh Flow', () => {
it('should refresh expired token and save new credentials', async () => {
const expiredCredentials: AuthCredentials = {
token: 'old-token',
refreshToken: 'old-refresh-token',
userId: 'test-user-id',
email: 'test@example.com',
expiresAt: new Date(Date.now() - 60000).toISOString(),
savedAt: new Date(Date.now() - 3600000).toISOString(),
selectedContext: {
orgId: 'test-org',
briefId: 'test-brief',
updatedAt: new Date().toISOString()
}
};
credentialStore.saveCredentials(expiredCredentials);
authManager = AuthManager.getInstance();
vi.spyOn(
authManager['supabaseClient'],
'refreshSession'
).mockResolvedValue(mockRefreshedSession);
const refreshedCredentials = await authManager.getCredentials();
expect(refreshedCredentials).not.toBeNull();
expect(refreshedCredentials?.token).toBe('new-access-token-xyz');
expect(refreshedCredentials?.refreshToken).toBe('new-refresh-token-xyz');
// Verify context was preserved
expect(refreshedCredentials?.selectedContext?.orgId).toBe('test-org');
expect(refreshedCredentials?.selectedContext?.briefId).toBe('test-brief');
// Verify new expiration is in the future
const newExpiry = new Date(refreshedCredentials!.expiresAt!).getTime();
const now = Date.now();
expect(newExpiry).toBeGreaterThan(now);
});
it('should return null if refresh fails', async () => {
const expiredCredentials: AuthCredentials = {
token: 'expired-token',
refreshToken: 'invalid-refresh-token',
userId: 'test-user-id',
email: 'test@example.com',
expiresAt: new Date(Date.now() - 60000).toISOString(),
savedAt: new Date().toISOString()
};
credentialStore.saveCredentials(expiredCredentials);
authManager = AuthManager.getInstance();
// Mock refresh to fail
vi.spyOn(
authManager['supabaseClient'],
'refreshSession'
).mockRejectedValue(new Error('Refresh token expired'));
const credentials = await authManager.getCredentials();
expect(credentials).toBeNull();
});
it('should return null if no refresh token available', async () => {
const expiredCredentials: AuthCredentials = {
token: 'expired-token',
// No refresh token
userId: 'test-user-id',
email: 'test@example.com',
expiresAt: new Date(Date.now() - 60000).toISOString(),
savedAt: new Date().toISOString()
};
credentialStore.saveCredentials(expiredCredentials);
authManager = AuthManager.getInstance();
const credentials = await authManager.getCredentials();
expect(credentials).toBeNull();
});
it('should return null if credentials missing expiresAt', async () => {
const credentialsWithoutExpiry: AuthCredentials = {
token: 'test-token',
refreshToken: 'refresh-token',
userId: 'test-user-id',
email: 'test@example.com',
// Missing expiresAt
savedAt: new Date().toISOString()
} as any;
credentialStore.saveCredentials(credentialsWithoutExpiry);
authManager = AuthManager.getInstance();
const credentials = await authManager.getCredentials();
// Should return null because no valid expiration
expect(credentials).toBeNull();
});
});
describe('Clock Skew Tolerance', () => {
it('should refresh token within 30-second expiry window', async () => {
// Token expires in 15 seconds (within 30-second buffer)
const almostExpiredCredentials: AuthCredentials = {
token: 'almost-expired-token',
refreshToken: 'valid-refresh-token',
userId: 'test-user-id',
email: 'test@example.com',
expiresAt: new Date(Date.now() + 15000).toISOString(), // 15 seconds from now
savedAt: new Date().toISOString()
};
credentialStore.saveCredentials(almostExpiredCredentials);
authManager = AuthManager.getInstance();
const mockRefreshSession = vi
.fn()
.mockResolvedValue(mockRefreshedSession);
vi.spyOn(
authManager['supabaseClient'],
'refreshSession'
).mockImplementation(mockRefreshSession);
const credentials = await authManager.getCredentials();
// Should trigger refresh due to 30-second buffer
expect(mockRefreshSession).toHaveBeenCalledTimes(1);
expect(credentials?.token).toBe('new-access-token-xyz');
});
it('should not refresh token well before expiry', async () => {
// Token expires in 5 minutes (well outside 30-second buffer)
const validCredentials: AuthCredentials = {
token: 'valid-token',
refreshToken: 'valid-refresh-token',
userId: 'test-user-id',
email: 'test@example.com',
expiresAt: new Date(Date.now() + 300000).toISOString(), // 5 minutes
savedAt: new Date().toISOString()
};
credentialStore.saveCredentials(validCredentials);
authManager = AuthManager.getInstance();
const mockRefreshSession = vi.fn();
vi.spyOn(
authManager['supabaseClient'],
'refreshSession'
).mockImplementation(mockRefreshSession);
const credentials = await authManager.getCredentials();
expect(mockRefreshSession).not.toHaveBeenCalled();
expect(credentials?.token).toBe('valid-token');
});
});
describe('Synchronous vs Async Methods', () => {
it('getCredentialsSync should not trigger refresh', () => {
const expiredCredentials: AuthCredentials = {
token: 'expired-token',
refreshToken: 'valid-refresh-token',
userId: 'test-user-id',
email: 'test@example.com',
expiresAt: new Date(Date.now() - 60000).toISOString(),
savedAt: new Date().toISOString()
};
credentialStore.saveCredentials(expiredCredentials);
authManager = AuthManager.getInstance();
// Synchronous call should return null without refresh
const credentials = authManager.getCredentialsSync();
expect(credentials).toBeNull();
});
it('getCredentials async should trigger refresh', async () => {
const expiredCredentials: AuthCredentials = {
token: 'expired-token',
refreshToken: 'valid-refresh-token',
userId: 'test-user-id',
email: 'test@example.com',
expiresAt: new Date(Date.now() - 60000).toISOString(),
savedAt: new Date().toISOString()
};
credentialStore.saveCredentials(expiredCredentials);
authManager = AuthManager.getInstance();
vi.spyOn(
authManager['supabaseClient'],
'refreshSession'
).mockResolvedValue(mockRefreshedSession);
const credentials = await authManager.getCredentials();
expect(credentials).not.toBeNull();
expect(credentials?.token).toBe('new-access-token-xyz');
});
});
describe('Multiple Concurrent Calls', () => {
it('should handle concurrent getCredentials calls gracefully', async () => {
const expiredCredentials: AuthCredentials = {
token: 'expired-token',
refreshToken: 'valid-refresh-token',
userId: 'test-user-id',
email: 'test@example.com',
expiresAt: new Date(Date.now() - 60000).toISOString(),
savedAt: new Date().toISOString()
};
credentialStore.saveCredentials(expiredCredentials);
authManager = AuthManager.getInstance();
const mockRefreshSession = vi
.fn()
.mockResolvedValue(mockRefreshedSession);
vi.spyOn(
authManager['supabaseClient'],
'refreshSession'
).mockImplementation(mockRefreshSession);
// Make multiple concurrent calls
const [creds1, creds2, creds3] = await Promise.all([
authManager.getCredentials(),
authManager.getCredentials(),
authManager.getCredentials()
]);
// All should get the refreshed token
expect(creds1?.token).toBe('new-access-token-xyz');
expect(creds2?.token).toBe('new-access-token-xyz');
expect(creds3?.token).toBe('new-access-token-xyz');
// Refresh might be called multiple times, but that's okay
// (ideally we'd debounce, but this is acceptable behavior)
expect(mockRefreshSession).toHaveBeenCalled();
});
});
});