Compare commits
1 Commits
task-maste
...
docs/auto-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
086ae62f77 |
7
.changeset/auto-update-changelog-highlights.md
Normal file
7
.changeset/auto-update-changelog-highlights.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"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.
|
||||
@@ -11,7 +11,6 @@
|
||||
"access": "public",
|
||||
"baseBranch": "main",
|
||||
"ignore": [
|
||||
"docs",
|
||||
"@tm/claude-code-plugin"
|
||||
"docs"
|
||||
]
|
||||
}
|
||||
47
.changeset/mean-planes-wave.md
Normal file
47
.changeset/mean-planes-wave.md
Normal file
@@ -0,0 +1,47 @@
|
||||
---
|
||||
"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!
|
||||
17
.changeset/nice-ways-hope.md
Normal file
17
.changeset/nice-ways-hope.md
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
"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.
|
||||
7
.changeset/plain-falcons-serve.md
Normal file
7
.changeset/plain-falcons-serve.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"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.
|
||||
16
.changeset/smart-owls-relax.md
Normal file
16
.changeset/smart-owls-relax.md
Normal file
@@ -0,0 +1,16 @@
|
||||
---
|
||||
"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.
|
||||
170
CHANGELOG.md
170
CHANGELOG.md
@@ -1,175 +1,5 @@
|
||||
# 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
|
||||
|
||||
@@ -310,12 +310,6 @@ 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">
|
||||
|
||||
@@ -11,13 +11,6 @@
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tm/core@null
|
||||
|
||||
## null
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @tm/core@null
|
||||
|
||||
|
||||
@@ -143,7 +143,7 @@ export class AuthCommand extends Command {
|
||||
*/
|
||||
private async executeStatus(): Promise<void> {
|
||||
try {
|
||||
const result = await this.displayStatus();
|
||||
const result = this.displayStatus();
|
||||
this.setLastResult(result);
|
||||
} catch (error: any) {
|
||||
this.handleError(error);
|
||||
@@ -171,8 +171,8 @@ export class AuthCommand extends Command {
|
||||
/**
|
||||
* Display authentication status
|
||||
*/
|
||||
private async displayStatus(): Promise<AuthResult> {
|
||||
const credentials = await this.authManager.getCredentials();
|
||||
private displayStatus(): AuthResult {
|
||||
const credentials = this.authManager.getCredentials();
|
||||
|
||||
console.log(chalk.cyan('\n🔐 Authentication Status\n'));
|
||||
|
||||
@@ -187,29 +187,19 @@ export class AuthCommand extends Command {
|
||||
if (credentials.expiresAt) {
|
||||
const expiresAt = new Date(credentials.expiresAt);
|
||||
const now = new Date();
|
||||
const timeRemaining = expiresAt.getTime() - now.getTime();
|
||||
const hoursRemaining = Math.floor(timeRemaining / (1000 * 60 * 60));
|
||||
const minutesRemaining = Math.floor(timeRemaining / (1000 * 60));
|
||||
const hoursRemaining = Math.floor(
|
||||
(expiresAt.getTime() - now.getTime()) / (1000 * 60 * 60)
|
||||
);
|
||||
|
||||
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
|
||||
if (hoursRemaining > 0) {
|
||||
console.log(
|
||||
chalk.yellow(` Expired at: ${expiresAt.toLocaleString()}`)
|
||||
chalk.gray(
|
||||
` Expires: ${expiresAt.toLocaleString()} (${hoursRemaining} hours remaining)`
|
||||
)
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
chalk.yellow(` Token expired at: ${expiresAt.toLocaleString()}`)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
@@ -325,7 +315,7 @@ export class AuthCommand extends Command {
|
||||
]);
|
||||
|
||||
if (!continueAuth) {
|
||||
const credentials = await this.authManager.getCredentials();
|
||||
const credentials = this.authManager.getCredentials();
|
||||
ui.displaySuccess('Using existing authentication');
|
||||
|
||||
if (credentials) {
|
||||
@@ -490,7 +480,7 @@ export class AuthCommand extends Command {
|
||||
/**
|
||||
* Get current credentials (for programmatic usage)
|
||||
*/
|
||||
getCredentials(): Promise<AuthCredentials | null> {
|
||||
getCredentials(): AuthCredentials | null {
|
||||
return this.authManager.getCredentials();
|
||||
}
|
||||
|
||||
|
||||
@@ -115,7 +115,7 @@ export class ContextCommand extends Command {
|
||||
*/
|
||||
private async executeShow(): Promise<void> {
|
||||
try {
|
||||
const result = await this.displayContext();
|
||||
const result = this.displayContext();
|
||||
this.setLastResult(result);
|
||||
} catch (error: any) {
|
||||
this.handleError(error);
|
||||
@@ -126,7 +126,7 @@ export class ContextCommand extends Command {
|
||||
/**
|
||||
* Display current context
|
||||
*/
|
||||
private async displayContext(): Promise<ContextResult> {
|
||||
private displayContext(): 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 = await this.authManager.getContext();
|
||||
const context = 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: (await this.authManager.getContext()) || undefined,
|
||||
context: 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 = await this.authManager.getContext();
|
||||
const context = 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: (await this.authManager.getContext()) || undefined,
|
||||
context: 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: (await this.authManager.getContext()) || undefined,
|
||||
context: this.authManager.getContext() || undefined,
|
||||
message: 'Cleared brief selection'
|
||||
};
|
||||
}
|
||||
@@ -508,7 +508,7 @@ export class ContextCommand extends Command {
|
||||
this.setLastResult({
|
||||
success: true,
|
||||
action: 'set',
|
||||
context: (await this.authManager.getContext()) || undefined,
|
||||
context: 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: (await this.authManager.getContext()) || undefined,
|
||||
context: 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(): Promise<UserContext | null> {
|
||||
getContext(): UserContext | null {
|
||||
return this.authManager.getContext();
|
||||
}
|
||||
|
||||
|
||||
@@ -103,7 +103,7 @@ export class ExportCommand extends Command {
|
||||
await this.initializeServices();
|
||||
|
||||
// Get current context
|
||||
const context = await this.authManager.getContext();
|
||||
const context = this.authManager.getContext();
|
||||
|
||||
// Determine org and brief IDs
|
||||
let orgId = options?.org || context?.orgId;
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# docs
|
||||
|
||||
## 0.0.6
|
||||
|
||||
## 0.0.5
|
||||
|
||||
## 0.0.4
|
||||
|
||||
@@ -33,6 +33,8 @@
|
||||
]
|
||||
},
|
||||
"getting-started/api-keys",
|
||||
"getting-started/claude-code-plugin",
|
||||
"getting-started/migration-plugin",
|
||||
"getting-started/faq",
|
||||
"getting-started/contribute"
|
||||
]
|
||||
|
||||
129
apps/docs/getting-started/claude-code-plugin.mdx
Normal file
129
apps/docs/getting-started/claude-code-plugin.mdx
Normal file
@@ -0,0 +1,129 @@
|
||||
# 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).
|
||||
144
apps/docs/getting-started/migration-plugin.mdx
Normal file
144
apps/docs/getting-started/migration-plugin.mdx
Normal file
@@ -0,0 +1,144 @@
|
||||
# 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!
|
||||
@@ -5,7 +5,36 @@ 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.
|
||||
|
||||
<Note>Cursor Users Can Use the One Click Install Below</Note>
|
||||
## 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>
|
||||
<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">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<Tip>
|
||||
Welcome to v1 of the Task Master Docs. Expect weekly updates as we expand and refine each section.
|
||||
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.
|
||||
</Tip>
|
||||
|
||||
We've organized the docs into three sections depending on your experience level and goals:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "docs",
|
||||
"version": "0.0.6",
|
||||
"version": "0.0.5",
|
||||
"private": true,
|
||||
"description": "Task Master documentation powered by Mintlify",
|
||||
"scripts": {
|
||||
|
||||
@@ -3,4 +3,26 @@ 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
|
||||
@@ -1,14 +1,5 @@
|
||||
# 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
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"private": true,
|
||||
"displayName": "TaskMaster",
|
||||
"description": "A visual Kanban board interface for TaskMaster projects in VS Code",
|
||||
"version": "0.25.6",
|
||||
"version": "0.25.5",
|
||||
"publisher": "Hamster",
|
||||
"icon": "assets/icon.png",
|
||||
"engines": {
|
||||
@@ -239,6 +239,9 @@
|
||||
"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",
|
||||
@@ -274,8 +277,7 @@
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwindcss": "4.1.11",
|
||||
"typescript": "^5.9.2",
|
||||
"@tm/core": "*",
|
||||
"task-master-ai": "*"
|
||||
"@tm/core": "*"
|
||||
},
|
||||
"overrides": {
|
||||
"glob@<8": "^10.4.5",
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 130 KiB |
70
output.txt
Normal file
70
output.txt
Normal file
File diff suppressed because one or more lines are too long
109
package-lock.json
generated
109
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "task-master-ai",
|
||||
"version": "0.29.0",
|
||||
"version": "0.28.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "task-master-ai",
|
||||
"version": "0.29.0",
|
||||
"version": "0.28.0",
|
||||
"license": "MIT WITH Commons-Clause",
|
||||
"workspaces": [
|
||||
"apps/*",
|
||||
@@ -125,13 +125,16 @@
|
||||
}
|
||||
},
|
||||
"apps/docs": {
|
||||
"version": "0.0.6",
|
||||
"version": "0.0.5",
|
||||
"devDependencies": {
|
||||
"mintlify": "^4.2.111"
|
||||
}
|
||||
},
|
||||
"apps/extension": {
|
||||
"version": "0.25.6",
|
||||
"version": "0.25.5",
|
||||
"dependencies": {
|
||||
"task-master-ai": "*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/modifiers": "^9.0.0",
|
||||
@@ -167,7 +170,6 @@
|
||||
"react-dom": "^19.0.0",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwindcss": "4.1.11",
|
||||
"task-master-ai": "*",
|
||||
"typescript": "^5.9.2"
|
||||
},
|
||||
"engines": {
|
||||
@@ -176,7 +178,6 @@
|
||||
},
|
||||
"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",
|
||||
@@ -194,7 +195,6 @@
|
||||
},
|
||||
"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,7 +209,6 @@
|
||||
},
|
||||
"apps/extension/node_modules/@ai-sdk/azure": {
|
||||
"version": "1.3.25",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@ai-sdk/openai": "1.3.24",
|
||||
@@ -225,7 +224,6 @@
|
||||
},
|
||||
"apps/extension/node_modules/@ai-sdk/google": {
|
||||
"version": "1.2.22",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@ai-sdk/provider": "1.1.3",
|
||||
@@ -240,7 +238,6 @@
|
||||
},
|
||||
"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",
|
||||
@@ -258,7 +255,6 @@
|
||||
},
|
||||
"apps/extension/node_modules/@ai-sdk/groq": {
|
||||
"version": "1.2.9",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@ai-sdk/provider": "1.1.3",
|
||||
@@ -273,7 +269,6 @@
|
||||
},
|
||||
"apps/extension/node_modules/@ai-sdk/mistral": {
|
||||
"version": "1.2.8",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@ai-sdk/provider": "1.1.3",
|
||||
@@ -288,7 +283,6 @@
|
||||
},
|
||||
"apps/extension/node_modules/@ai-sdk/openai": {
|
||||
"version": "1.3.24",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@ai-sdk/provider": "1.1.3",
|
||||
@@ -303,7 +297,6 @@
|
||||
},
|
||||
"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",
|
||||
@@ -318,7 +311,6 @@
|
||||
},
|
||||
"apps/extension/node_modules/@ai-sdk/perplexity": {
|
||||
"version": "1.1.9",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@ai-sdk/provider": "1.1.3",
|
||||
@@ -333,7 +325,6 @@
|
||||
},
|
||||
"apps/extension/node_modules/@ai-sdk/provider": {
|
||||
"version": "1.1.3",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"json-schema": "^0.4.0"
|
||||
@@ -344,7 +335,6 @@
|
||||
},
|
||||
"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",
|
||||
@@ -360,7 +350,6 @@
|
||||
},
|
||||
"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",
|
||||
@@ -383,7 +372,6 @@
|
||||
},
|
||||
"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",
|
||||
@@ -399,7 +387,6 @@
|
||||
},
|
||||
"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",
|
||||
@@ -415,7 +402,6 @@
|
||||
},
|
||||
"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",
|
||||
@@ -430,7 +416,6 @@
|
||||
},
|
||||
"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"
|
||||
@@ -441,7 +426,6 @@
|
||||
},
|
||||
"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",
|
||||
@@ -463,7 +447,6 @@
|
||||
},
|
||||
"apps/extension/node_modules/ai": {
|
||||
"version": "4.3.19",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@ai-sdk/provider": "1.1.3",
|
||||
@@ -488,7 +471,6 @@
|
||||
},
|
||||
"apps/extension/node_modules/ai-sdk-provider-gemini-cli": {
|
||||
"version": "0.1.3",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -522,7 +504,6 @@
|
||||
},
|
||||
"apps/extension/node_modules/ollama-ai-provider": {
|
||||
"version": "1.2.0",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@ai-sdk/provider": "^1.0.0",
|
||||
@@ -543,7 +524,6 @@
|
||||
},
|
||||
"apps/extension/node_modules/openai": {
|
||||
"version": "4.104.0",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@types/node": "^18.11.18",
|
||||
@@ -572,7 +552,6 @@
|
||||
},
|
||||
"apps/extension/node_modules/openai/node_modules/@types/node": {
|
||||
"version": "18.19.127",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
@@ -580,12 +559,10 @@
|
||||
},
|
||||
"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/*",
|
||||
@@ -657,7 +634,6 @@
|
||||
},
|
||||
"apps/extension/node_modules/zod": {
|
||||
"version": "3.25.76",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
@@ -665,7 +641,6 @@
|
||||
},
|
||||
"apps/extension/node_modules/zod-to-json-schema": {
|
||||
"version": "3.24.6",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"peerDependencies": {
|
||||
"zod": "^3.24.1"
|
||||
@@ -954,7 +929,6 @@
|
||||
},
|
||||
"node_modules/@anthropic-ai/sdk": {
|
||||
"version": "0.39.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "^18.11.18",
|
||||
@@ -968,7 +942,6 @@
|
||||
},
|
||||
"node_modules/@anthropic-ai/sdk/node_modules/@types/node": {
|
||||
"version": "18.19.127",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
@@ -976,7 +949,6 @@
|
||||
},
|
||||
"node_modules/@anthropic-ai/sdk/node_modules/undici-types": {
|
||||
"version": "5.26.5",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@ark/schema": {
|
||||
@@ -8436,7 +8408,6 @@
|
||||
},
|
||||
"node_modules/@types/diff-match-patch": {
|
||||
"version": "1.0.36",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/es-aggregate-error": {
|
||||
@@ -8607,7 +8578,6 @@
|
||||
},
|
||||
"node_modules/@types/node-fetch": {
|
||||
"version": "2.6.13",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
@@ -9053,7 +9023,6 @@
|
||||
},
|
||||
"node_modules/abort-controller": {
|
||||
"version": "3.0.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"event-target-shim": "^5.0.0"
|
||||
@@ -9115,7 +9084,6 @@
|
||||
},
|
||||
"node_modules/agentkeepalive": {
|
||||
"version": "4.6.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"humanize-ms": "^1.2.1"
|
||||
@@ -9698,7 +9666,6 @@
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/auto-bind": {
|
||||
@@ -11476,7 +11443,6 @@
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
@@ -12115,7 +12081,6 @@
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
@@ -12138,7 +12103,6 @@
|
||||
},
|
||||
"node_modules/dequal": {
|
||||
"version": "2.0.3",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
@@ -12258,7 +12222,6 @@
|
||||
},
|
||||
"node_modules/diff-match-patch": {
|
||||
"version": "1.0.5",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/diff-sequences": {
|
||||
@@ -12758,7 +12721,6 @@
|
||||
},
|
||||
"node_modules/es-set-tostringtag": {
|
||||
"version": "2.1.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
@@ -13047,7 +13009,6 @@
|
||||
},
|
||||
"node_modules/event-target-shim": {
|
||||
"version": "5.0.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
@@ -14086,7 +14047,6 @@
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.4",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
@@ -14101,7 +14061,6 @@
|
||||
},
|
||||
"node_modules/form-data-encoder": {
|
||||
"version": "1.7.2",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/format": {
|
||||
@@ -14113,7 +14072,6 @@
|
||||
},
|
||||
"node_modules/formdata-node": {
|
||||
"version": "4.4.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"node-domexception": "1.0.0",
|
||||
@@ -14774,7 +14732,6 @@
|
||||
},
|
||||
"node_modules/has-tostringtag": {
|
||||
"version": "1.0.2",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-symbols": "^1.0.3"
|
||||
@@ -15349,7 +15306,6 @@
|
||||
},
|
||||
"node_modules/humanize-ms": {
|
||||
"version": "1.2.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.0.0"
|
||||
@@ -18136,7 +18092,6 @@
|
||||
},
|
||||
"node_modules/jsondiffpatch": {
|
||||
"version": "0.6.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/diff-match-patch": "^1.0.36",
|
||||
@@ -20318,7 +20273,6 @@
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.11",
|
||||
"devOptional": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -20427,7 +20381,6 @@
|
||||
},
|
||||
"node_modules/node-domexception": {
|
||||
"version": "1.0.0",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -21292,7 +21245,6 @@
|
||||
},
|
||||
"node_modules/partial-json": {
|
||||
"version": "0.1.7",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/patch-console": {
|
||||
@@ -22065,7 +22017,6 @@
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "19.1.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -23192,7 +23143,6 @@
|
||||
},
|
||||
"node_modules/secure-json-parse": {
|
||||
"version": "2.7.0",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/selderee": {
|
||||
@@ -24240,26 +24190,6 @@
|
||||
"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": [
|
||||
@@ -24437,7 +24367,6 @@
|
||||
},
|
||||
"node_modules/swr": {
|
||||
"version": "2.3.6",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dequal": "^2.0.3",
|
||||
@@ -24630,7 +24559,6 @@
|
||||
},
|
||||
"node_modules/throttleit": {
|
||||
"version": "2.1.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
@@ -25733,7 +25661,6 @@
|
||||
},
|
||||
"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"
|
||||
@@ -26142,7 +26069,6 @@
|
||||
},
|
||||
"node_modules/web-streams-polyfill": {
|
||||
"version": "4.0.0-beta.3",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
@@ -27136,8 +27062,22 @@
|
||||
},
|
||||
"packages/claude-code-plugin": {
|
||||
"name": "@tm/claude-code-plugin",
|
||||
"version": "0.0.2",
|
||||
"license": "MIT WITH Commons-Clause"
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"packages/tm-core": {
|
||||
"name": "@tm/core",
|
||||
@@ -27149,7 +27089,6 @@
|
||||
"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"
|
||||
}
|
||||
@@ -27459,8 +27398,6 @@
|
||||
},
|
||||
"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": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "task-master-ai",
|
||||
"version": "0.29.0",
|
||||
"version": "0.28.0",
|
||||
"description": "A task management system for ambitious AI-driven development that doesn't overwhelm and confuse Cursor.",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
# @tm/ai-sdk-provider-grok-cli
|
||||
|
||||
## null
|
||||
|
||||
## null
|
||||
|
||||
@@ -4,6 +4,4 @@
|
||||
|
||||
## null
|
||||
|
||||
## null
|
||||
|
||||
## 1.0.1
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
# @tm/claude-code-plugin
|
||||
|
||||
## 0.0.2
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@tm/claude-code-plugin",
|
||||
"version": "0.0.2",
|
||||
"version": "0.0.1",
|
||||
"description": "Task Master AI plugin for Claude Code - AI-powered task management with commands, agents, and MCP integration",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
|
||||
## null
|
||||
|
||||
## null
|
||||
|
||||
## 0.26.1
|
||||
|
||||
All notable changes to the @task-master/tm-core package will be documented in this file.
|
||||
|
||||
@@ -37,8 +37,7 @@
|
||||
"@types/node": "^22.10.5",
|
||||
"@vitest/coverage-v8": "^3.2.4",
|
||||
"typescript": "^5.9.2",
|
||||
"vitest": "^3.2.4",
|
||||
"strip-literal": "3.1.0"
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"files": ["src", "README.md", "CHANGELOG.md"],
|
||||
"keywords": ["task-management", "typescript", "ai", "prd", "parser"],
|
||||
|
||||
@@ -21,16 +21,11 @@ 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(_options?: any) {
|
||||
getCredentials() {
|
||||
return null;
|
||||
}
|
||||
saveCredentials() {}
|
||||
@@ -90,7 +85,7 @@ describe('AuthManager Singleton', () => {
|
||||
expect(instance1).toBe(instance2);
|
||||
});
|
||||
|
||||
it('should use config on first call', async () => {
|
||||
it('should use config on first call', () => {
|
||||
const config = {
|
||||
baseUrl: 'https://test.auth.com',
|
||||
configDir: '/test/config',
|
||||
@@ -106,7 +101,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 = await instance.getCredentials();
|
||||
const credentials = instance.getCredentials();
|
||||
expect(credentials).toBeNull(); // File doesn't exist, but config was propagated correctly
|
||||
});
|
||||
|
||||
@@ -29,8 +29,6 @@ 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);
|
||||
@@ -38,10 +36,7 @@ export class AuthManager {
|
||||
this.oauthService = new OAuthService(this.credentialStore, config);
|
||||
|
||||
// Initialize Supabase client with session restoration
|
||||
// Fire-and-forget with catch handler to prevent unhandled rejections
|
||||
this.initializeSupabaseSession().catch(() => {
|
||||
// Errors are already logged in initializeSupabaseSession
|
||||
});
|
||||
this.initializeSupabaseSession();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -83,60 +78,8 @@ export class AuthManager {
|
||||
|
||||
/**
|
||||
* Get stored authentication credentials
|
||||
* Automatically refreshes the token if expired
|
||||
*/
|
||||
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 {
|
||||
getCredentials(): AuthCredentials | null {
|
||||
return this.credentialStore.getCredentials();
|
||||
}
|
||||
|
||||
@@ -228,8 +171,8 @@ export class AuthManager {
|
||||
/**
|
||||
* Get the current user context (org/brief selection)
|
||||
*/
|
||||
async getContext(): Promise<UserContext | null> {
|
||||
const credentials = await this.getCredentials();
|
||||
getContext(): UserContext | null {
|
||||
const credentials = this.getCredentials();
|
||||
return credentials?.selectedContext || null;
|
||||
}
|
||||
|
||||
@@ -237,7 +180,7 @@ export class AuthManager {
|
||||
* Update the user context (org/brief selection)
|
||||
*/
|
||||
async updateContext(context: Partial<UserContext>): Promise<void> {
|
||||
const credentials = await this.getCredentials();
|
||||
const credentials = this.getCredentials();
|
||||
if (!credentials) {
|
||||
throw new AuthenticationError('Not authenticated', 'NOT_AUTHENTICATED');
|
||||
}
|
||||
@@ -263,7 +206,7 @@ export class AuthManager {
|
||||
* Clear the user context
|
||||
*/
|
||||
async clearContext(): Promise<void> {
|
||||
const credentials = await this.getCredentials();
|
||||
const credentials = this.getCredentials();
|
||||
if (!credentials) {
|
||||
throw new AuthenticationError('Not authenticated', 'NOT_AUTHENTICATED');
|
||||
}
|
||||
@@ -280,7 +223,7 @@ export class AuthManager {
|
||||
private async getOrganizationService(): Promise<OrganizationService> {
|
||||
if (!this.organizationService) {
|
||||
// First check if we have credentials with a token
|
||||
const credentials = await this.getCredentials();
|
||||
const credentials = this.getCredentials();
|
||||
if (!credentials || !credentials.token) {
|
||||
throw new AuthenticationError('Not authenticated', 'NOT_AUTHENTICATED');
|
||||
}
|
||||
|
||||
@@ -1,289 +0,0 @@
|
||||
/**
|
||||
* @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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -47,8 +47,8 @@ export class SupabaseTaskRepository {
|
||||
* Gets the current brief ID from auth context
|
||||
* @throws {Error} If no brief is selected
|
||||
*/
|
||||
private async getBriefIdOrThrow(): Promise<string> {
|
||||
const context = await this.authManager.getContext();
|
||||
private getBriefIdOrThrow(): string {
|
||||
const context = 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 = await this.getBriefIdOrThrow();
|
||||
const briefId = 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 = await this.getBriefIdOrThrow();
|
||||
const briefId = 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 = await this.getBriefIdOrThrow();
|
||||
const briefId = this.getBriefIdOrThrow();
|
||||
|
||||
// Validate updates using Zod schema
|
||||
try {
|
||||
|
||||
@@ -105,7 +105,7 @@ export class ExportService {
|
||||
}
|
||||
|
||||
// Get current context
|
||||
const context = await this.authManager.getContext();
|
||||
const context = 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 = await this.authManager.getContext();
|
||||
const context = 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`;
|
||||
const apiUrl = `${process.env.TM_PUBLIC_BASE_DOMAIN}/ai/api/v1/briefs/${briefId}/tasks/bulk`;
|
||||
|
||||
// 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 = await this.authManager.getCredentials();
|
||||
const credentials = this.authManager.getCredentials();
|
||||
if (!credentials || !credentials.token) {
|
||||
throw new Error('Not authenticated');
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ export class ApiStorage implements IStorage {
|
||||
private async loadTagsIntoCache(): Promise<void> {
|
||||
try {
|
||||
const authManager = AuthManager.getInstance();
|
||||
const context = await authManager.getContext();
|
||||
const context = 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 = await authManager.getContext();
|
||||
const context = 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 = await authManager.getContext();
|
||||
const context = authManager.getContext();
|
||||
|
||||
// In our API-based system, we only have one "tag" at a time - the current brief
|
||||
if (context?.briefId) {
|
||||
|
||||
@@ -72,8 +72,8 @@ export class StorageFactory {
|
||||
{ storageType: 'api', missing }
|
||||
);
|
||||
}
|
||||
// Use auth token from AuthManager (synchronous - no auto-refresh here)
|
||||
const credentials = authManager.getCredentialsSync();
|
||||
// Use auth token from AuthManager
|
||||
const credentials = authManager.getCredentials();
|
||||
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.getCredentialsSync();
|
||||
const credentials = authManager.getCredentials();
|
||||
if (credentials) {
|
||||
// Configure API storage with auth credentials
|
||||
const nextStorage: StorageSettings = {
|
||||
|
||||
@@ -1,176 +0,0 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -1,392 +0,0 @@
|
||||
/**
|
||||
* @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();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user