Compare commits
1 Commits
main
...
docs/auto-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
533420a17e |
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",
|
"access": "public",
|
||||||
"baseBranch": "main",
|
"baseBranch": "main",
|
||||||
"ignore": [
|
"ignore": [
|
||||||
"docs",
|
"docs"
|
||||||
"@tm/claude-code-plugin"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
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 (`/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!
|
||||||
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.
|
||||||
21
.changeset/pre.json
Normal file
21
.changeset/pre.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"mode": "exit",
|
||||||
|
"tag": "rc",
|
||||||
|
"initialVersions": {
|
||||||
|
"task-master-ai": "0.28.0",
|
||||||
|
"@tm/cli": "",
|
||||||
|
"docs": "0.0.5",
|
||||||
|
"extension": "0.25.5",
|
||||||
|
"@tm/ai-sdk-provider-grok-cli": "",
|
||||||
|
"@tm/build-config": "",
|
||||||
|
"@tm/claude-code-plugin": "0.0.1",
|
||||||
|
"@tm/core": ""
|
||||||
|
},
|
||||||
|
"changesets": [
|
||||||
|
"auto-update-changelog-highlights",
|
||||||
|
"mean-planes-wave",
|
||||||
|
"nice-ways-hope",
|
||||||
|
"plain-falcons-serve",
|
||||||
|
"smart-owls-relax"
|
||||||
|
]
|
||||||
|
}
|
||||||
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.
|
||||||
89
CHANGELOG.md
89
CHANGELOG.md
@@ -1,94 +1,5 @@
|
|||||||
# task-master-ai
|
# 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
|
## 0.29.0-rc.0
|
||||||
|
|
||||||
### Minor Changes
|
### Minor Changes
|
||||||
|
|||||||
@@ -310,12 +310,6 @@ cd claude-task-master
|
|||||||
node scripts/init.js
|
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
|
## Contributors
|
||||||
|
|
||||||
<a href="https://github.com/eyaltoledano/claude-task-master/graphs/contributors">
|
<a href="https://github.com/eyaltoledano/claude-task-master/graphs/contributors">
|
||||||
|
|||||||
@@ -11,13 +11,6 @@
|
|||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|
||||||
- Updated dependencies []:
|
|
||||||
- @tm/core@null
|
|
||||||
|
|
||||||
## null
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- Updated dependencies []:
|
- Updated dependencies []:
|
||||||
- @tm/core@null
|
- @tm/core@null
|
||||||
|
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ export class AuthCommand extends Command {
|
|||||||
*/
|
*/
|
||||||
private async executeStatus(): Promise<void> {
|
private async executeStatus(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const result = await this.displayStatus();
|
const result = this.displayStatus();
|
||||||
this.setLastResult(result);
|
this.setLastResult(result);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
this.handleError(error);
|
this.handleError(error);
|
||||||
@@ -171,8 +171,8 @@ export class AuthCommand extends Command {
|
|||||||
/**
|
/**
|
||||||
* Display authentication status
|
* Display authentication status
|
||||||
*/
|
*/
|
||||||
private async displayStatus(): Promise<AuthResult> {
|
private displayStatus(): AuthResult {
|
||||||
const credentials = await this.authManager.getCredentials();
|
const credentials = this.authManager.getCredentials();
|
||||||
|
|
||||||
console.log(chalk.cyan('\n🔐 Authentication Status\n'));
|
console.log(chalk.cyan('\n🔐 Authentication Status\n'));
|
||||||
|
|
||||||
@@ -187,29 +187,19 @@ export class AuthCommand extends Command {
|
|||||||
if (credentials.expiresAt) {
|
if (credentials.expiresAt) {
|
||||||
const expiresAt = new Date(credentials.expiresAt);
|
const expiresAt = new Date(credentials.expiresAt);
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const timeRemaining = expiresAt.getTime() - now.getTime();
|
const hoursRemaining = Math.floor(
|
||||||
const hoursRemaining = Math.floor(timeRemaining / (1000 * 60 * 60));
|
(expiresAt.getTime() - now.getTime()) / (1000 * 60 * 60)
|
||||||
const minutesRemaining = Math.floor(timeRemaining / (1000 * 60));
|
);
|
||||||
|
|
||||||
if (timeRemaining > 0) {
|
if (hoursRemaining > 0) {
|
||||||
// Token is still valid
|
|
||||||
if (hoursRemaining > 0) {
|
|
||||||
console.log(
|
|
||||||
chalk.gray(
|
|
||||||
` Expires at: ${expiresAt.toLocaleString()} (${hoursRemaining} hours remaining)`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
console.log(
|
|
||||||
chalk.gray(
|
|
||||||
` Expires at: ${expiresAt.toLocaleString()} (${minutesRemaining} minutes remaining)`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Token has expired
|
|
||||||
console.log(
|
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 {
|
} else {
|
||||||
@@ -325,7 +315,7 @@ export class AuthCommand extends Command {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
if (!continueAuth) {
|
if (!continueAuth) {
|
||||||
const credentials = await this.authManager.getCredentials();
|
const credentials = this.authManager.getCredentials();
|
||||||
ui.displaySuccess('Using existing authentication');
|
ui.displaySuccess('Using existing authentication');
|
||||||
|
|
||||||
if (credentials) {
|
if (credentials) {
|
||||||
@@ -490,7 +480,7 @@ export class AuthCommand extends Command {
|
|||||||
/**
|
/**
|
||||||
* Get current credentials (for programmatic usage)
|
* Get current credentials (for programmatic usage)
|
||||||
*/
|
*/
|
||||||
getCredentials(): Promise<AuthCredentials | null> {
|
getCredentials(): AuthCredentials | null {
|
||||||
return this.authManager.getCredentials();
|
return this.authManager.getCredentials();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ export class ContextCommand extends Command {
|
|||||||
*/
|
*/
|
||||||
private async executeShow(): Promise<void> {
|
private async executeShow(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const result = await this.displayContext();
|
const result = this.displayContext();
|
||||||
this.setLastResult(result);
|
this.setLastResult(result);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
this.handleError(error);
|
this.handleError(error);
|
||||||
@@ -126,7 +126,7 @@ export class ContextCommand extends Command {
|
|||||||
/**
|
/**
|
||||||
* Display current context
|
* Display current context
|
||||||
*/
|
*/
|
||||||
private async displayContext(): Promise<ContextResult> {
|
private displayContext(): ContextResult {
|
||||||
// Check authentication first
|
// Check authentication first
|
||||||
if (!this.authManager.isAuthenticated()) {
|
if (!this.authManager.isAuthenticated()) {
|
||||||
console.log(chalk.yellow('✗ Not authenticated'));
|
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'));
|
console.log(chalk.cyan('\n🌍 Workspace Context\n'));
|
||||||
|
|
||||||
@@ -263,7 +263,7 @@ export class ContextCommand extends Command {
|
|||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
action: 'select-org',
|
action: 'select-org',
|
||||||
context: (await this.authManager.getContext()) || undefined,
|
context: this.authManager.getContext() || undefined,
|
||||||
message: `Selected organization: ${selectedOrg.name}`
|
message: `Selected organization: ${selectedOrg.name}`
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -284,7 +284,7 @@ export class ContextCommand extends Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if org is selected
|
// Check if org is selected
|
||||||
const context = await this.authManager.getContext();
|
const context = this.authManager.getContext();
|
||||||
if (!context?.orgId) {
|
if (!context?.orgId) {
|
||||||
ui.displayError(
|
ui.displayError(
|
||||||
'No organization selected. Run "tm context org" first.'
|
'No organization selected. Run "tm context org" first.'
|
||||||
@@ -353,7 +353,7 @@ export class ContextCommand extends Command {
|
|||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
action: 'select-brief',
|
action: 'select-brief',
|
||||||
context: (await this.authManager.getContext()) || undefined,
|
context: this.authManager.getContext() || undefined,
|
||||||
message: `Selected brief: ${selectedBrief.name}`
|
message: `Selected brief: ${selectedBrief.name}`
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
@@ -368,7 +368,7 @@ export class ContextCommand extends Command {
|
|||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
action: 'select-brief',
|
action: 'select-brief',
|
||||||
context: (await this.authManager.getContext()) || undefined,
|
context: this.authManager.getContext() || undefined,
|
||||||
message: 'Cleared brief selection'
|
message: 'Cleared brief selection'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -508,7 +508,7 @@ export class ContextCommand extends Command {
|
|||||||
this.setLastResult({
|
this.setLastResult({
|
||||||
success: true,
|
success: true,
|
||||||
action: 'set',
|
action: 'set',
|
||||||
context: (await this.authManager.getContext()) || undefined,
|
context: this.authManager.getContext() || undefined,
|
||||||
message: 'Context set from brief'
|
message: 'Context set from brief'
|
||||||
});
|
});
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@@ -631,7 +631,7 @@ export class ContextCommand extends Command {
|
|||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
action: 'set',
|
action: 'set',
|
||||||
context: (await this.authManager.getContext()) || undefined,
|
context: this.authManager.getContext() || undefined,
|
||||||
message: 'Context updated'
|
message: 'Context updated'
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -682,7 +682,7 @@ export class ContextCommand extends Command {
|
|||||||
/**
|
/**
|
||||||
* Get current context (for programmatic usage)
|
* Get current context (for programmatic usage)
|
||||||
*/
|
*/
|
||||||
getContext(): Promise<UserContext | null> {
|
getContext(): UserContext | null {
|
||||||
return this.authManager.getContext();
|
return this.authManager.getContext();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ export class ExportCommand extends Command {
|
|||||||
await this.initializeServices();
|
await this.initializeServices();
|
||||||
|
|
||||||
// Get current context
|
// Get current context
|
||||||
const context = await this.authManager.getContext();
|
const context = this.authManager.getContext();
|
||||||
|
|
||||||
// Determine org and brief IDs
|
// Determine org and brief IDs
|
||||||
let orgId = options?.org || context?.orgId;
|
let orgId = options?.org || context?.orgId;
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
# docs
|
# docs
|
||||||
|
|
||||||
## 0.0.6
|
|
||||||
|
|
||||||
## 0.0.5
|
## 0.0.5
|
||||||
|
|
||||||
## 0.0.4
|
## 0.0.4
|
||||||
|
|||||||
@@ -31,9 +31,23 @@ cursor://anysphere.cursor-deeplink/mcp/install?name=taskmaster-ai&config=eyJjb21
|
|||||||
|
|
||||||
> **Note:** After clicking the link, you'll still need to add your API keys to the configuration. The link installs the MCP server with placeholder keys that you'll need to replace with your actual API keys.
|
> **Note:** After clicking the link, you'll still need to add your API keys to the configuration. The link installs the MCP server with placeholder keys that you'll need to replace with your actual API keys.
|
||||||
|
|
||||||
### Claude Code Quick Install
|
### Claude Code Plugin Install (Recommended)
|
||||||
|
|
||||||
For Claude Code users:
|
For Claude Code users, install via the plugin marketplace:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/plugin marketplace add eyaltoledano/claude-task-master
|
||||||
|
/plugin install taskmaster@taskmaster
|
||||||
|
```
|
||||||
|
|
||||||
|
This provides:
|
||||||
|
- **49 slash commands** with clean naming (`/taskmaster:command-name`)
|
||||||
|
- **3 specialized AI agents** (task-orchestrator, task-executor, task-checker)
|
||||||
|
- **Automatic updates** when new features are released
|
||||||
|
|
||||||
|
### Claude Code MCP Alternative
|
||||||
|
|
||||||
|
You can also use MCP directly:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
claude mcp add taskmaster-ai -- npx -y task-master-ai
|
claude mcp add taskmaster-ai -- npx -y task-master-ai
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "docs",
|
"name": "docs",
|
||||||
"version": "0.0.6",
|
"version": "0.0.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Task Master documentation powered by Mintlify",
|
"description": "Task Master documentation powered by Mintlify",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -3,4 +3,44 @@ title: "What's New"
|
|||||||
sidebarTitle: "What's New"
|
sidebarTitle: "What's New"
|
||||||
---
|
---
|
||||||
|
|
||||||
An easy way to see the latest releases
|
## 🎉 New: Claude Code Plugin Support
|
||||||
|
|
||||||
|
Task Master AI now supports Claude Code plugins with modern marketplace distribution!
|
||||||
|
|
||||||
|
### What's New
|
||||||
|
|
||||||
|
- **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
|
||||||
|
```
|
||||||
|
|
||||||
|
### Migration for Existing Users
|
||||||
|
|
||||||
|
The `rules add claude` command no longer copies files to `.claude/` directories. Instead:
|
||||||
|
|
||||||
|
- Shows plugin installation instructions
|
||||||
|
- Only manages CLAUDE.md imports for agent instructions
|
||||||
|
- Directs users to install the official plugin
|
||||||
|
|
||||||
|
If you previously used `rules add claude`:
|
||||||
|
|
||||||
|
1. Old commands in `.claude/commands/` will continue working but won't receive updates
|
||||||
|
2. Install the plugin for 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!
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
# Change Log
|
# Change Log
|
||||||
|
|
||||||
## 0.25.6
|
|
||||||
|
|
||||||
## 0.25.6-rc.0
|
## 0.25.6-rc.0
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"displayName": "TaskMaster",
|
"displayName": "TaskMaster",
|
||||||
"description": "A visual Kanban board interface for TaskMaster projects in VS Code",
|
"description": "A visual Kanban board interface for TaskMaster projects in VS Code",
|
||||||
"version": "0.25.6",
|
"version": "0.25.6-rc.0",
|
||||||
"publisher": "Hamster",
|
"publisher": "Hamster",
|
||||||
"icon": "assets/icon.png",
|
"icon": "assets/icon.png",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -239,6 +239,9 @@
|
|||||||
"watch:css": "npx @tailwindcss/cli -i ./src/webview/index.css -o ./dist/index.css --watch",
|
"watch:css": "npx @tailwindcss/cli -i ./src/webview/index.css -o ./dist/index.css --watch",
|
||||||
"check-types": "tsc --noEmit"
|
"check-types": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"task-master-ai": "0.29.0-rc.0"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@dnd-kit/modifiers": "^9.0.0",
|
"@dnd-kit/modifiers": "^9.0.0",
|
||||||
@@ -274,8 +277,7 @@
|
|||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"tailwindcss": "4.1.11",
|
"tailwindcss": "4.1.11",
|
||||||
"typescript": "^5.9.2",
|
"typescript": "^5.9.2",
|
||||||
"@tm/core": "*",
|
"@tm/core": "*"
|
||||||
"task-master-ai": "*"
|
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"glob@<8": "^10.4.5",
|
"glob@<8": "^10.4.5",
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 130 KiB |
30
output.txt
Normal file
30
output.txt
Normal file
File diff suppressed because one or more lines are too long
143
package-lock.json
generated
143
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "task-master-ai",
|
"name": "task-master-ai",
|
||||||
"version": "0.29.0",
|
"version": "npm:task-master-ai@0.29.0-rc.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "task-master-ai",
|
"name": "task-master-ai",
|
||||||
"version": "0.29.0",
|
"version": "0.29.0-rc.0",
|
||||||
"license": "MIT WITH Commons-Clause",
|
"license": "MIT WITH Commons-Clause",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"apps/*",
|
"apps/*",
|
||||||
@@ -125,58 +125,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"apps/docs": {
|
"apps/docs": {
|
||||||
"version": "0.0.6",
|
"version": "0.0.5",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"mintlify": "^4.2.111"
|
"mintlify": "^4.2.111"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"apps/extension": {
|
|
||||||
"version": "0.25.6",
|
|
||||||
"devDependencies": {
|
|
||||||
"@dnd-kit/core": "^6.3.1",
|
|
||||||
"@dnd-kit/modifiers": "^9.0.0",
|
|
||||||
"@modelcontextprotocol/sdk": "1.13.3",
|
|
||||||
"@radix-ui/react-collapsible": "^1.1.11",
|
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
|
||||||
"@radix-ui/react-label": "^2.1.7",
|
|
||||||
"@radix-ui/react-portal": "^1.1.9",
|
|
||||||
"@radix-ui/react-scroll-area": "^1.2.9",
|
|
||||||
"@radix-ui/react-separator": "^1.1.7",
|
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
|
||||||
"@tailwindcss/postcss": "^4.1.11",
|
|
||||||
"@tanstack/react-query": "^5.83.0",
|
|
||||||
"@tm/core": "*",
|
|
||||||
"@types/mocha": "^10.0.10",
|
|
||||||
"@types/node": "^22.10.5",
|
|
||||||
"@types/react": "19.1.8",
|
|
||||||
"@types/react-dom": "19.1.6",
|
|
||||||
"@types/vscode": "^1.101.0",
|
|
||||||
"@vscode/test-cli": "^0.0.11",
|
|
||||||
"@vscode/test-electron": "^2.5.2",
|
|
||||||
"@vscode/vsce": "^2.32.0",
|
|
||||||
"autoprefixer": "10.4.21",
|
|
||||||
"class-variance-authority": "^0.7.1",
|
|
||||||
"clsx": "^2.1.1",
|
|
||||||
"esbuild": "^0.25.3",
|
|
||||||
"esbuild-postcss": "^0.0.4",
|
|
||||||
"fs-extra": "^11.3.0",
|
|
||||||
"lucide-react": "^0.525.0",
|
|
||||||
"npm-run-all": "^4.1.5",
|
|
||||||
"postcss": "8.5.6",
|
|
||||||
"react": "^19.0.0",
|
|
||||||
"react-dom": "^19.0.0",
|
|
||||||
"tailwind-merge": "^3.3.1",
|
|
||||||
"tailwindcss": "4.1.11",
|
|
||||||
"task-master-ai": "*",
|
|
||||||
"typescript": "^5.9.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"vscode": "^1.93.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"apps/extension/node_modules/@ai-sdk/amazon-bedrock": {
|
"apps/extension/node_modules/@ai-sdk/amazon-bedrock": {
|
||||||
"version": "2.2.12",
|
"version": "2.2.12",
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/provider": "1.1.3",
|
"@ai-sdk/provider": "1.1.3",
|
||||||
@@ -194,7 +149,6 @@
|
|||||||
},
|
},
|
||||||
"apps/extension/node_modules/@ai-sdk/anthropic": {
|
"apps/extension/node_modules/@ai-sdk/anthropic": {
|
||||||
"version": "1.2.12",
|
"version": "1.2.12",
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/provider": "1.1.3",
|
"@ai-sdk/provider": "1.1.3",
|
||||||
@@ -209,7 +163,6 @@
|
|||||||
},
|
},
|
||||||
"apps/extension/node_modules/@ai-sdk/azure": {
|
"apps/extension/node_modules/@ai-sdk/azure": {
|
||||||
"version": "1.3.25",
|
"version": "1.3.25",
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/openai": "1.3.24",
|
"@ai-sdk/openai": "1.3.24",
|
||||||
@@ -225,7 +178,6 @@
|
|||||||
},
|
},
|
||||||
"apps/extension/node_modules/@ai-sdk/google": {
|
"apps/extension/node_modules/@ai-sdk/google": {
|
||||||
"version": "1.2.22",
|
"version": "1.2.22",
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/provider": "1.1.3",
|
"@ai-sdk/provider": "1.1.3",
|
||||||
@@ -240,7 +192,6 @@
|
|||||||
},
|
},
|
||||||
"apps/extension/node_modules/@ai-sdk/google-vertex": {
|
"apps/extension/node_modules/@ai-sdk/google-vertex": {
|
||||||
"version": "2.2.27",
|
"version": "2.2.27",
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/anthropic": "1.2.12",
|
"@ai-sdk/anthropic": "1.2.12",
|
||||||
@@ -258,7 +209,6 @@
|
|||||||
},
|
},
|
||||||
"apps/extension/node_modules/@ai-sdk/groq": {
|
"apps/extension/node_modules/@ai-sdk/groq": {
|
||||||
"version": "1.2.9",
|
"version": "1.2.9",
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/provider": "1.1.3",
|
"@ai-sdk/provider": "1.1.3",
|
||||||
@@ -273,7 +223,6 @@
|
|||||||
},
|
},
|
||||||
"apps/extension/node_modules/@ai-sdk/mistral": {
|
"apps/extension/node_modules/@ai-sdk/mistral": {
|
||||||
"version": "1.2.8",
|
"version": "1.2.8",
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/provider": "1.1.3",
|
"@ai-sdk/provider": "1.1.3",
|
||||||
@@ -288,7 +237,6 @@
|
|||||||
},
|
},
|
||||||
"apps/extension/node_modules/@ai-sdk/openai": {
|
"apps/extension/node_modules/@ai-sdk/openai": {
|
||||||
"version": "1.3.24",
|
"version": "1.3.24",
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/provider": "1.1.3",
|
"@ai-sdk/provider": "1.1.3",
|
||||||
@@ -303,7 +251,6 @@
|
|||||||
},
|
},
|
||||||
"apps/extension/node_modules/@ai-sdk/openai-compatible": {
|
"apps/extension/node_modules/@ai-sdk/openai-compatible": {
|
||||||
"version": "0.2.16",
|
"version": "0.2.16",
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/provider": "1.1.3",
|
"@ai-sdk/provider": "1.1.3",
|
||||||
@@ -318,7 +265,6 @@
|
|||||||
},
|
},
|
||||||
"apps/extension/node_modules/@ai-sdk/perplexity": {
|
"apps/extension/node_modules/@ai-sdk/perplexity": {
|
||||||
"version": "1.1.9",
|
"version": "1.1.9",
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/provider": "1.1.3",
|
"@ai-sdk/provider": "1.1.3",
|
||||||
@@ -333,7 +279,6 @@
|
|||||||
},
|
},
|
||||||
"apps/extension/node_modules/@ai-sdk/provider": {
|
"apps/extension/node_modules/@ai-sdk/provider": {
|
||||||
"version": "1.1.3",
|
"version": "1.1.3",
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"json-schema": "^0.4.0"
|
"json-schema": "^0.4.0"
|
||||||
@@ -344,7 +289,6 @@
|
|||||||
},
|
},
|
||||||
"apps/extension/node_modules/@ai-sdk/provider-utils": {
|
"apps/extension/node_modules/@ai-sdk/provider-utils": {
|
||||||
"version": "2.2.8",
|
"version": "2.2.8",
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/provider": "1.1.3",
|
"@ai-sdk/provider": "1.1.3",
|
||||||
@@ -360,7 +304,6 @@
|
|||||||
},
|
},
|
||||||
"apps/extension/node_modules/@ai-sdk/react": {
|
"apps/extension/node_modules/@ai-sdk/react": {
|
||||||
"version": "1.2.12",
|
"version": "1.2.12",
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/provider-utils": "2.2.8",
|
"@ai-sdk/provider-utils": "2.2.8",
|
||||||
@@ -383,7 +326,6 @@
|
|||||||
},
|
},
|
||||||
"apps/extension/node_modules/@ai-sdk/ui-utils": {
|
"apps/extension/node_modules/@ai-sdk/ui-utils": {
|
||||||
"version": "1.2.11",
|
"version": "1.2.11",
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/provider": "1.1.3",
|
"@ai-sdk/provider": "1.1.3",
|
||||||
@@ -399,7 +341,6 @@
|
|||||||
},
|
},
|
||||||
"apps/extension/node_modules/@ai-sdk/xai": {
|
"apps/extension/node_modules/@ai-sdk/xai": {
|
||||||
"version": "1.2.18",
|
"version": "1.2.18",
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/openai-compatible": "0.2.16",
|
"@ai-sdk/openai-compatible": "0.2.16",
|
||||||
@@ -415,7 +356,6 @@
|
|||||||
},
|
},
|
||||||
"apps/extension/node_modules/@openrouter/ai-sdk-provider": {
|
"apps/extension/node_modules/@openrouter/ai-sdk-provider": {
|
||||||
"version": "0.4.6",
|
"version": "0.4.6",
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/provider": "1.0.9",
|
"@ai-sdk/provider": "1.0.9",
|
||||||
@@ -430,7 +370,6 @@
|
|||||||
},
|
},
|
||||||
"apps/extension/node_modules/@openrouter/ai-sdk-provider/node_modules/@ai-sdk/provider": {
|
"apps/extension/node_modules/@openrouter/ai-sdk-provider/node_modules/@ai-sdk/provider": {
|
||||||
"version": "1.0.9",
|
"version": "1.0.9",
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"json-schema": "^0.4.0"
|
"json-schema": "^0.4.0"
|
||||||
@@ -441,7 +380,6 @@
|
|||||||
},
|
},
|
||||||
"apps/extension/node_modules/@openrouter/ai-sdk-provider/node_modules/@ai-sdk/provider-utils": {
|
"apps/extension/node_modules/@openrouter/ai-sdk-provider/node_modules/@ai-sdk/provider-utils": {
|
||||||
"version": "2.1.10",
|
"version": "2.1.10",
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/provider": "1.0.9",
|
"@ai-sdk/provider": "1.0.9",
|
||||||
@@ -463,7 +401,6 @@
|
|||||||
},
|
},
|
||||||
"apps/extension/node_modules/ai": {
|
"apps/extension/node_modules/ai": {
|
||||||
"version": "4.3.19",
|
"version": "4.3.19",
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/provider": "1.1.3",
|
"@ai-sdk/provider": "1.1.3",
|
||||||
@@ -488,7 +425,6 @@
|
|||||||
},
|
},
|
||||||
"apps/extension/node_modules/ai-sdk-provider-gemini-cli": {
|
"apps/extension/node_modules/ai-sdk-provider-gemini-cli": {
|
||||||
"version": "0.1.3",
|
"version": "0.1.3",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -522,7 +458,6 @@
|
|||||||
},
|
},
|
||||||
"apps/extension/node_modules/ollama-ai-provider": {
|
"apps/extension/node_modules/ollama-ai-provider": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/provider": "^1.0.0",
|
"@ai-sdk/provider": "^1.0.0",
|
||||||
@@ -543,7 +478,6 @@
|
|||||||
},
|
},
|
||||||
"apps/extension/node_modules/openai": {
|
"apps/extension/node_modules/openai": {
|
||||||
"version": "4.104.0",
|
"version": "4.104.0",
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/node": "^18.11.18",
|
"@types/node": "^18.11.18",
|
||||||
@@ -572,7 +506,6 @@
|
|||||||
},
|
},
|
||||||
"apps/extension/node_modules/openai/node_modules/@types/node": {
|
"apps/extension/node_modules/openai/node_modules/@types/node": {
|
||||||
"version": "18.19.127",
|
"version": "18.19.127",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~5.26.4"
|
"undici-types": "~5.26.4"
|
||||||
@@ -580,12 +513,10 @@
|
|||||||
},
|
},
|
||||||
"apps/extension/node_modules/openai/node_modules/undici-types": {
|
"apps/extension/node_modules/openai/node_modules/undici-types": {
|
||||||
"version": "5.26.5",
|
"version": "5.26.5",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"apps/extension/node_modules/task-master-ai": {
|
"apps/extension/node_modules/task-master-ai": {
|
||||||
"version": "0.27.1",
|
"version": "0.27.1",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT WITH Commons-Clause",
|
"license": "MIT WITH Commons-Clause",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"apps/*",
|
"apps/*",
|
||||||
@@ -657,7 +588,6 @@
|
|||||||
},
|
},
|
||||||
"apps/extension/node_modules/zod": {
|
"apps/extension/node_modules/zod": {
|
||||||
"version": "3.25.76",
|
"version": "3.25.76",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
@@ -665,7 +595,6 @@
|
|||||||
},
|
},
|
||||||
"apps/extension/node_modules/zod-to-json-schema": {
|
"apps/extension/node_modules/zod-to-json-schema": {
|
||||||
"version": "3.24.6",
|
"version": "3.24.6",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"zod": "^3.24.1"
|
"zod": "^3.24.1"
|
||||||
@@ -954,7 +883,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/@anthropic-ai/sdk": {
|
"node_modules/@anthropic-ai/sdk": {
|
||||||
"version": "0.39.0",
|
"version": "0.39.0",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/node": "^18.11.18",
|
"@types/node": "^18.11.18",
|
||||||
@@ -968,7 +896,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/@anthropic-ai/sdk/node_modules/@types/node": {
|
"node_modules/@anthropic-ai/sdk/node_modules/@types/node": {
|
||||||
"version": "18.19.127",
|
"version": "18.19.127",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~5.26.4"
|
"undici-types": "~5.26.4"
|
||||||
@@ -976,7 +903,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/@anthropic-ai/sdk/node_modules/undici-types": {
|
"node_modules/@anthropic-ai/sdk/node_modules/undici-types": {
|
||||||
"version": "5.26.5",
|
"version": "5.26.5",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@ark/schema": {
|
"node_modules/@ark/schema": {
|
||||||
@@ -8436,7 +8362,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/@types/diff-match-patch": {
|
"node_modules/@types/diff-match-patch": {
|
||||||
"version": "1.0.36",
|
"version": "1.0.36",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/es-aggregate-error": {
|
"node_modules/@types/es-aggregate-error": {
|
||||||
@@ -8607,7 +8532,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/@types/node-fetch": {
|
"node_modules/@types/node-fetch": {
|
||||||
"version": "2.6.13",
|
"version": "2.6.13",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/node": "*",
|
"@types/node": "*",
|
||||||
@@ -9053,7 +8977,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/abort-controller": {
|
"node_modules/abort-controller": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"event-target-shim": "^5.0.0"
|
"event-target-shim": "^5.0.0"
|
||||||
@@ -9115,7 +9038,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/agentkeepalive": {
|
"node_modules/agentkeepalive": {
|
||||||
"version": "4.6.0",
|
"version": "4.6.0",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"humanize-ms": "^1.2.1"
|
"humanize-ms": "^1.2.1"
|
||||||
@@ -9698,7 +9620,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/asynckit": {
|
"node_modules/asynckit": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/auto-bind": {
|
"node_modules/auto-bind": {
|
||||||
@@ -11476,7 +11397,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/combined-stream": {
|
"node_modules/combined-stream": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"delayed-stream": "~1.0.0"
|
"delayed-stream": "~1.0.0"
|
||||||
@@ -12115,7 +12035,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/delayed-stream": {
|
"node_modules/delayed-stream": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.4.0"
|
"node": ">=0.4.0"
|
||||||
@@ -12138,7 +12057,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/dequal": {
|
"node_modules/dequal": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
@@ -12258,7 +12176,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/diff-match-patch": {
|
"node_modules/diff-match-patch": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/diff-sequences": {
|
"node_modules/diff-sequences": {
|
||||||
@@ -12758,7 +12675,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/es-set-tostringtag": {
|
"node_modules/es-set-tostringtag": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"es-errors": "^1.3.0",
|
"es-errors": "^1.3.0",
|
||||||
@@ -13047,7 +12963,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/event-target-shim": {
|
"node_modules/event-target-shim": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
@@ -14086,7 +14001,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/form-data": {
|
"node_modules/form-data": {
|
||||||
"version": "4.0.4",
|
"version": "4.0.4",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"asynckit": "^0.4.0",
|
"asynckit": "^0.4.0",
|
||||||
@@ -14101,7 +14015,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/form-data-encoder": {
|
"node_modules/form-data-encoder": {
|
||||||
"version": "1.7.2",
|
"version": "1.7.2",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/format": {
|
"node_modules/format": {
|
||||||
@@ -14113,7 +14026,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/formdata-node": {
|
"node_modules/formdata-node": {
|
||||||
"version": "4.4.1",
|
"version": "4.4.1",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"node-domexception": "1.0.0",
|
"node-domexception": "1.0.0",
|
||||||
@@ -14774,7 +14686,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/has-tostringtag": {
|
"node_modules/has-tostringtag": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"has-symbols": "^1.0.3"
|
"has-symbols": "^1.0.3"
|
||||||
@@ -15349,7 +15260,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/humanize-ms": {
|
"node_modules/humanize-ms": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ms": "^2.0.0"
|
"ms": "^2.0.0"
|
||||||
@@ -18136,7 +18046,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/jsondiffpatch": {
|
"node_modules/jsondiffpatch": {
|
||||||
"version": "0.6.0",
|
"version": "0.6.0",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/diff-match-patch": "^1.0.36",
|
"@types/diff-match-patch": "^1.0.36",
|
||||||
@@ -18400,7 +18309,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"darwin"
|
"darwin"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 12.0.0"
|
"node": ">= 12.0.0"
|
||||||
},
|
},
|
||||||
@@ -20318,7 +20226,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.11",
|
"version": "3.3.11",
|
||||||
"devOptional": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -20427,7 +20334,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/node-domexception": {
|
"node_modules/node-domexception": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -21292,7 +21198,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/partial-json": {
|
"node_modules/partial-json": {
|
||||||
"version": "0.1.7",
|
"version": "0.1.7",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/patch-console": {
|
"node_modules/patch-console": {
|
||||||
@@ -22065,7 +21970,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/react": {
|
"node_modules/react": {
|
||||||
"version": "19.1.1",
|
"version": "19.1.1",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
@@ -23192,7 +23096,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/secure-json-parse": {
|
"node_modules/secure-json-parse": {
|
||||||
"version": "2.7.0",
|
"version": "2.7.0",
|
||||||
"dev": true,
|
|
||||||
"license": "BSD-3-Clause"
|
"license": "BSD-3-Clause"
|
||||||
},
|
},
|
||||||
"node_modules/selderee": {
|
"node_modules/selderee": {
|
||||||
@@ -24240,26 +24143,6 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"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": {
|
"node_modules/strnum": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"funding": [
|
"funding": [
|
||||||
@@ -24437,7 +24320,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/swr": {
|
"node_modules/swr": {
|
||||||
"version": "2.3.6",
|
"version": "2.3.6",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dequal": "^2.0.3",
|
"dequal": "^2.0.3",
|
||||||
@@ -24630,7 +24512,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/throttleit": {
|
"node_modules/throttleit": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
@@ -25733,7 +25614,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/use-sync-external-store": {
|
"node_modules/use-sync-external-store": {
|
||||||
"version": "1.5.0",
|
"version": "1.5.0",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
@@ -25986,7 +25866,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"darwin"
|
"darwin"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
@@ -26142,7 +26021,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/web-streams-polyfill": {
|
"node_modules/web-streams-polyfill": {
|
||||||
"version": "4.0.0-beta.3",
|
"version": "4.0.0-beta.3",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 14"
|
"node": ">= 14"
|
||||||
@@ -27136,9 +27014,19 @@
|
|||||||
},
|
},
|
||||||
"packages/claude-code-plugin": {
|
"packages/claude-code-plugin": {
|
||||||
"name": "@tm/claude-code-plugin",
|
"name": "@tm/claude-code-plugin",
|
||||||
"version": "0.0.2",
|
"version": "0.0.1",
|
||||||
"license": "MIT WITH Commons-Clause"
|
"license": "MIT WITH Commons-Clause"
|
||||||
},
|
},
|
||||||
|
"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==",
|
||||||
|
"extraneous": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~6.21.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"packages/tm-core": {
|
"packages/tm-core": {
|
||||||
"name": "@tm/core",
|
"name": "@tm/core",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -27149,7 +27037,6 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^22.10.5",
|
"@types/node": "^22.10.5",
|
||||||
"@vitest/coverage-v8": "^3.2.4",
|
"@vitest/coverage-v8": "^3.2.4",
|
||||||
"strip-literal": "3.1.0",
|
|
||||||
"typescript": "^5.9.2",
|
"typescript": "^5.9.2",
|
||||||
"vitest": "^3.2.4"
|
"vitest": "^3.2.4"
|
||||||
}
|
}
|
||||||
@@ -27459,8 +27346,6 @@
|
|||||||
},
|
},
|
||||||
"packages/tm-core/node_modules/vitest": {
|
"packages/tm-core/node_modules/vitest": {
|
||||||
"version": "3.2.4",
|
"version": "3.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz",
|
|
||||||
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
|
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "task-master-ai",
|
"name": "task-master-ai",
|
||||||
"version": "0.29.0",
|
"version": "0.29.0-rc.0",
|
||||||
"description": "A task management system for ambitious AI-driven development that doesn't overwhelm and confuse Cursor.",
|
"description": "A task management system for ambitious AI-driven development that doesn't overwhelm and confuse Cursor.",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
# @tm/ai-sdk-provider-grok-cli
|
# @tm/ai-sdk-provider-grok-cli
|
||||||
|
|
||||||
## null
|
## null
|
||||||
|
|
||||||
## null
|
|
||||||
|
|||||||
@@ -4,6 +4,4 @@
|
|||||||
|
|
||||||
## null
|
## null
|
||||||
|
|
||||||
## null
|
|
||||||
|
|
||||||
## 1.0.1
|
## 1.0.1
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
# @tm/claude-code-plugin
|
|
||||||
|
|
||||||
## 0.0.2
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@tm/claude-code-plugin",
|
"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",
|
"description": "Task Master AI plugin for Claude Code - AI-powered task management with commands, agents, and MCP integration",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
|||||||
@@ -4,8 +4,6 @@
|
|||||||
|
|
||||||
## null
|
## null
|
||||||
|
|
||||||
## null
|
|
||||||
|
|
||||||
## 0.26.1
|
## 0.26.1
|
||||||
|
|
||||||
All notable changes to the @task-master/tm-core package will be documented in this file.
|
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",
|
"@types/node": "^22.10.5",
|
||||||
"@vitest/coverage-v8": "^3.2.4",
|
"@vitest/coverage-v8": "^3.2.4",
|
||||||
"typescript": "^5.9.2",
|
"typescript": "^5.9.2",
|
||||||
"vitest": "^3.2.4",
|
"vitest": "^3.2.4"
|
||||||
"strip-literal": "3.1.0"
|
|
||||||
},
|
},
|
||||||
"files": ["src", "README.md", "CHANGELOG.md"],
|
"files": ["src", "README.md", "CHANGELOG.md"],
|
||||||
"keywords": ["task-management", "typescript", "ai", "prd", "parser"],
|
"keywords": ["task-management", "typescript", "ai", "prd", "parser"],
|
||||||
|
|||||||
@@ -21,16 +21,11 @@ const CredentialStoreSpy = vi.fn();
|
|||||||
vi.mock('./credential-store.js', () => {
|
vi.mock('./credential-store.js', () => {
|
||||||
return {
|
return {
|
||||||
CredentialStore: class {
|
CredentialStore: class {
|
||||||
static getInstance(config?: any) {
|
|
||||||
return new (this as any)(config);
|
|
||||||
}
|
|
||||||
static resetInstance() {
|
|
||||||
// Mock reset instance method
|
|
||||||
}
|
|
||||||
constructor(config: any) {
|
constructor(config: any) {
|
||||||
CredentialStoreSpy(config);
|
CredentialStoreSpy(config);
|
||||||
|
this.getCredentials = vi.fn(() => null);
|
||||||
}
|
}
|
||||||
getCredentials(_options?: any) {
|
getCredentials() {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
saveCredentials() {}
|
saveCredentials() {}
|
||||||
@@ -90,7 +85,7 @@ describe('AuthManager Singleton', () => {
|
|||||||
expect(instance1).toBe(instance2);
|
expect(instance1).toBe(instance2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should use config on first call', async () => {
|
it('should use config on first call', () => {
|
||||||
const config = {
|
const config = {
|
||||||
baseUrl: 'https://test.auth.com',
|
baseUrl: 'https://test.auth.com',
|
||||||
configDir: '/test/config',
|
configDir: '/test/config',
|
||||||
@@ -106,7 +101,7 @@ describe('AuthManager Singleton', () => {
|
|||||||
|
|
||||||
// Verify the config is passed to internal components through observable behavior
|
// Verify the config is passed to internal components through observable behavior
|
||||||
// getCredentials would look in the configured file path
|
// 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
|
expect(credentials).toBeNull(); // File doesn't exist, but config was propagated correctly
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -29,8 +29,6 @@ export class AuthManager {
|
|||||||
private oauthService: OAuthService;
|
private oauthService: OAuthService;
|
||||||
private supabaseClient: SupabaseAuthClient;
|
private supabaseClient: SupabaseAuthClient;
|
||||||
private organizationService?: OrganizationService;
|
private organizationService?: OrganizationService;
|
||||||
private logger = getLogger('AuthManager');
|
|
||||||
private refreshPromise: Promise<AuthCredentials> | null = null;
|
|
||||||
|
|
||||||
private constructor(config?: Partial<AuthConfig>) {
|
private constructor(config?: Partial<AuthConfig>) {
|
||||||
this.credentialStore = CredentialStore.getInstance(config);
|
this.credentialStore = CredentialStore.getInstance(config);
|
||||||
@@ -38,10 +36,7 @@ export class AuthManager {
|
|||||||
this.oauthService = new OAuthService(this.credentialStore, config);
|
this.oauthService = new OAuthService(this.credentialStore, config);
|
||||||
|
|
||||||
// Initialize Supabase client with session restoration
|
// Initialize Supabase client with session restoration
|
||||||
// Fire-and-forget with catch handler to prevent unhandled rejections
|
this.initializeSupabaseSession();
|
||||||
this.initializeSupabaseSession().catch(() => {
|
|
||||||
// Errors are already logged in initializeSupabaseSession
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -83,60 +78,8 @@ export class AuthManager {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get stored authentication credentials
|
* Get stored authentication credentials
|
||||||
* Automatically refreshes the token if expired
|
|
||||||
*/
|
*/
|
||||||
async getCredentials(): Promise<AuthCredentials | null> {
|
getCredentials(): AuthCredentials | null {
|
||||||
const credentials = this.credentialStore.getCredentials({
|
|
||||||
allowExpired: true
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!credentials) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if credentials are expired (with 30-second clock skew buffer)
|
|
||||||
const CLOCK_SKEW_MS = 30_000;
|
|
||||||
const isExpired = credentials.expiresAt
|
|
||||||
? new Date(credentials.expiresAt).getTime() <= Date.now() + CLOCK_SKEW_MS
|
|
||||||
: false;
|
|
||||||
|
|
||||||
// If expired and we have a refresh token, attempt refresh
|
|
||||||
if (isExpired && credentials.refreshToken) {
|
|
||||||
// Return existing refresh promise if one is in progress
|
|
||||||
if (this.refreshPromise) {
|
|
||||||
try {
|
|
||||||
return await this.refreshPromise;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
this.logger.info('Token expired, attempting automatic refresh...');
|
|
||||||
this.refreshPromise = this.refreshToken();
|
|
||||||
const result = await this.refreshPromise;
|
|
||||||
return result;
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.warn('Automatic token refresh failed:', error);
|
|
||||||
return null;
|
|
||||||
} finally {
|
|
||||||
this.refreshPromise = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return null if expired and no refresh token
|
|
||||||
if (isExpired) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return credentials;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get stored authentication credentials (synchronous version)
|
|
||||||
* Does not attempt automatic refresh
|
|
||||||
*/
|
|
||||||
getCredentialsSync(): AuthCredentials | null {
|
|
||||||
return this.credentialStore.getCredentials();
|
return this.credentialStore.getCredentials();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,8 +171,8 @@ export class AuthManager {
|
|||||||
/**
|
/**
|
||||||
* Get the current user context (org/brief selection)
|
* Get the current user context (org/brief selection)
|
||||||
*/
|
*/
|
||||||
async getContext(): Promise<UserContext | null> {
|
getContext(): UserContext | null {
|
||||||
const credentials = await this.getCredentials();
|
const credentials = this.getCredentials();
|
||||||
return credentials?.selectedContext || null;
|
return credentials?.selectedContext || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,7 +180,7 @@ export class AuthManager {
|
|||||||
* Update the user context (org/brief selection)
|
* Update the user context (org/brief selection)
|
||||||
*/
|
*/
|
||||||
async updateContext(context: Partial<UserContext>): Promise<void> {
|
async updateContext(context: Partial<UserContext>): Promise<void> {
|
||||||
const credentials = await this.getCredentials();
|
const credentials = this.getCredentials();
|
||||||
if (!credentials) {
|
if (!credentials) {
|
||||||
throw new AuthenticationError('Not authenticated', 'NOT_AUTHENTICATED');
|
throw new AuthenticationError('Not authenticated', 'NOT_AUTHENTICATED');
|
||||||
}
|
}
|
||||||
@@ -263,7 +206,7 @@ export class AuthManager {
|
|||||||
* Clear the user context
|
* Clear the user context
|
||||||
*/
|
*/
|
||||||
async clearContext(): Promise<void> {
|
async clearContext(): Promise<void> {
|
||||||
const credentials = await this.getCredentials();
|
const credentials = this.getCredentials();
|
||||||
if (!credentials) {
|
if (!credentials) {
|
||||||
throw new AuthenticationError('Not authenticated', 'NOT_AUTHENTICATED');
|
throw new AuthenticationError('Not authenticated', 'NOT_AUTHENTICATED');
|
||||||
}
|
}
|
||||||
@@ -280,7 +223,7 @@ export class AuthManager {
|
|||||||
private async getOrganizationService(): Promise<OrganizationService> {
|
private async getOrganizationService(): Promise<OrganizationService> {
|
||||||
if (!this.organizationService) {
|
if (!this.organizationService) {
|
||||||
// First check if we have credentials with a token
|
// First check if we have credentials with a token
|
||||||
const credentials = await this.getCredentials();
|
const credentials = this.getCredentials();
|
||||||
if (!credentials || !credentials.token) {
|
if (!credentials || !credentials.token) {
|
||||||
throw new AuthenticationError('Not authenticated', 'NOT_AUTHENTICATED');
|
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
|
* Gets the current brief ID from auth context
|
||||||
* @throws {Error} If no brief is selected
|
* @throws {Error} If no brief is selected
|
||||||
*/
|
*/
|
||||||
private async getBriefIdOrThrow(): Promise<string> {
|
private getBriefIdOrThrow(): string {
|
||||||
const context = await this.authManager.getContext();
|
const context = this.authManager.getContext();
|
||||||
if (!context?.briefId) {
|
if (!context?.briefId) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'No brief selected. Please select a brief first using: tm context brief'
|
'No brief selected. Please select a brief first using: tm context brief'
|
||||||
@@ -61,7 +61,7 @@ export class SupabaseTaskRepository {
|
|||||||
_projectId?: string,
|
_projectId?: string,
|
||||||
options?: LoadTasksOptions
|
options?: LoadTasksOptions
|
||||||
): Promise<Task[]> {
|
): Promise<Task[]> {
|
||||||
const briefId = await this.getBriefIdOrThrow();
|
const briefId = this.getBriefIdOrThrow();
|
||||||
|
|
||||||
// Build query with filters
|
// Build query with filters
|
||||||
let query = this.supabase
|
let query = this.supabase
|
||||||
@@ -114,7 +114,7 @@ export class SupabaseTaskRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getTask(_projectId: string, taskId: string): Promise<Task | null> {
|
async getTask(_projectId: string, taskId: string): Promise<Task | null> {
|
||||||
const briefId = await this.getBriefIdOrThrow();
|
const briefId = this.getBriefIdOrThrow();
|
||||||
|
|
||||||
const { data, error } = await this.supabase
|
const { data, error } = await this.supabase
|
||||||
.from('tasks')
|
.from('tasks')
|
||||||
@@ -157,7 +157,7 @@ export class SupabaseTaskRepository {
|
|||||||
taskId: string,
|
taskId: string,
|
||||||
updates: Partial<Task>
|
updates: Partial<Task>
|
||||||
): Promise<Task> {
|
): Promise<Task> {
|
||||||
const briefId = await this.getBriefIdOrThrow();
|
const briefId = this.getBriefIdOrThrow();
|
||||||
|
|
||||||
// Validate updates using Zod schema
|
// Validate updates using Zod schema
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ export class ExportService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get current context
|
// Get current context
|
||||||
const context = await this.authManager.getContext();
|
const context = this.authManager.getContext();
|
||||||
|
|
||||||
// Determine org and brief IDs
|
// Determine org and brief IDs
|
||||||
let orgId = options.orgId || context?.orgId;
|
let orgId = options.orgId || context?.orgId;
|
||||||
@@ -232,7 +232,7 @@ export class ExportService {
|
|||||||
hasBrief: boolean;
|
hasBrief: boolean;
|
||||||
context: UserContext | null;
|
context: UserContext | null;
|
||||||
}> {
|
}> {
|
||||||
const context = await this.authManager.getContext();
|
const context = this.authManager.getContext();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
hasOrg: !!context?.orgId,
|
hasOrg: !!context?.orgId,
|
||||||
@@ -362,7 +362,7 @@ export class ExportService {
|
|||||||
|
|
||||||
if (useAPIEndpoint) {
|
if (useAPIEndpoint) {
|
||||||
// Use the new bulk import API endpoint
|
// 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
|
// Transform tasks to flat structure for API
|
||||||
const flatTasks = this.transformTasksForBulkImport(tasks);
|
const flatTasks = this.transformTasksForBulkImport(tasks);
|
||||||
@@ -370,16 +370,16 @@ export class ExportService {
|
|||||||
// Prepare request body
|
// Prepare request body
|
||||||
const requestBody = {
|
const requestBody = {
|
||||||
source: 'task-master-cli',
|
source: 'task-master-cli',
|
||||||
|
accountId: orgId,
|
||||||
options: {
|
options: {
|
||||||
dryRun: false,
|
dryRun: false,
|
||||||
stopOnError: false
|
stopOnError: false
|
||||||
},
|
},
|
||||||
accountId: orgId,
|
|
||||||
tasks: flatTasks
|
tasks: flatTasks
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get auth token
|
// Get auth token
|
||||||
const credentials = await this.authManager.getCredentials();
|
const credentials = this.authManager.getCredentials();
|
||||||
if (!credentials || !credentials.token) {
|
if (!credentials || !credentials.token) {
|
||||||
throw new Error('Not authenticated');
|
throw new Error('Not authenticated');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ export class ApiStorage implements IStorage {
|
|||||||
private async loadTagsIntoCache(): Promise<void> {
|
private async loadTagsIntoCache(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const authManager = AuthManager.getInstance();
|
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 we have a selected brief, create a virtual "tag" for it
|
||||||
if (context?.briefId) {
|
if (context?.briefId) {
|
||||||
@@ -152,7 +152,7 @@ export class ApiStorage implements IStorage {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const authManager = AuthManager.getInstance();
|
const authManager = AuthManager.getInstance();
|
||||||
const context = await authManager.getContext();
|
const context = authManager.getContext();
|
||||||
|
|
||||||
// If no brief is selected in context, throw an error
|
// If no brief is selected in context, throw an error
|
||||||
if (!context?.briefId) {
|
if (!context?.briefId) {
|
||||||
@@ -318,7 +318,7 @@ export class ApiStorage implements IStorage {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const authManager = AuthManager.getInstance();
|
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
|
// In our API-based system, we only have one "tag" at a time - the current brief
|
||||||
if (context?.briefId) {
|
if (context?.briefId) {
|
||||||
|
|||||||
@@ -72,8 +72,8 @@ export class StorageFactory {
|
|||||||
{ storageType: 'api', missing }
|
{ storageType: 'api', missing }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// Use auth token from AuthManager (synchronous - no auto-refresh here)
|
// Use auth token from AuthManager
|
||||||
const credentials = authManager.getCredentialsSync();
|
const credentials = authManager.getCredentials();
|
||||||
if (credentials) {
|
if (credentials) {
|
||||||
// Merge with existing storage config, ensuring required fields
|
// Merge with existing storage config, ensuring required fields
|
||||||
const nextStorage: StorageSettings = {
|
const nextStorage: StorageSettings = {
|
||||||
@@ -103,7 +103,7 @@ export class StorageFactory {
|
|||||||
|
|
||||||
// Then check if authenticated via AuthManager
|
// Then check if authenticated via AuthManager
|
||||||
if (authManager.isAuthenticated()) {
|
if (authManager.isAuthenticated()) {
|
||||||
const credentials = authManager.getCredentialsSync();
|
const credentials = authManager.getCredentials();
|
||||||
if (credentials) {
|
if (credentials) {
|
||||||
// Configure API storage with auth credentials
|
// Configure API storage with auth credentials
|
||||||
const nextStorage: StorageSettings = {
|
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