Skip to content

Commit 8e5ae0e

Browse files
authored
feat(tiers): add action-level tier requirements for consolidated tools (#77)
* feat(tiers): add action-level tier requirements for consolidated tools Add action-level tier requirements mapping: - Add ToolActionRequirements interface for action-specific tiers - Add actionRequirements map with all consolidated tools - Add getActionRequirement() for action-specific tier lookups - Add getHighestTier() to get max tier for any tool action - Add getTierRestrictedActions() to list premium/ultimate actions - Update getToolRequirement() to support action parameter - Update isToolAvailable() to check action-specific tiers - Update getToolTierInfo() in list-tools.ts for action tiers - Add Tier column to actions table in --export output - Update tests for new tier functionality Closes #48 * fix(tiers): fix string escaping, mixed tier logic, and add action-level tests - Use replaceAll for complete bracket removal in tier display - Fix mixed tier detection to compare default vs highest tier - Add tests for getActionRequirement, getHighestTier, getTierRestrictedActions - Add tests for isToolAvailable with action parameter * fix(tiers): extract TIER_ORDER constant and fix getAvailableTools - Extract TIER_ORDER constant to avoid duplication in tier comparison methods - Fix getAvailableTools to include tools from both legacy and action requirements - Add JSDoc documenting asterisk notation for mixed tiers in getTierBadge - Replace replaceAll with regex replace for broader Node.js compatibility - Add tests for getToolRequirement with action parameter - Add tests for isToolAvailable version checking with actions - Add tests for getAvailableTools combining both requirement sources
1 parent c906046 commit 8e5ae0e

File tree

4 files changed

+579
-32
lines changed

4 files changed

+579
-32
lines changed

src/cli/list-tools.ts

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -294,16 +294,53 @@ function printEnvironmentInfo(): void {
294294
console.log();
295295
}
296296

297-
function getToolTierInfo(toolName: string): string {
298-
const requirement = ToolAvailability.getToolRequirement(toolName);
299-
if (!requirement) return "";
297+
/**
298+
* Get tier information badge for a tool or action.
299+
*
300+
* @param toolName - The tool name
301+
* @param action - Optional action name for action-specific tier
302+
* @returns Tier badge string, e.g., "[tier: Premium]" or "[tier: Premium*]"
303+
*
304+
* Badge format:
305+
* - `[tier: Free]` - All actions available in Free tier
306+
* - `[tier: Premium]` - All actions require Premium tier
307+
* - `[tier: Premium*]` - Asterisk (*) indicates mixed tiers: the tool has some
308+
* actions requiring a higher tier than the default. For example, a tool with
309+
* default Free tier but some Premium-only actions shows "Premium*".
310+
*/
311+
function getToolTierInfo(toolName: string, action?: string): string {
312+
// For action-specific queries, get exact tier
313+
if (action) {
314+
const requirement = ToolAvailability.getToolRequirement(toolName, action);
315+
if (!requirement) return "";
316+
317+
const tierBadge =
318+
{
319+
free: "Free",
320+
premium: "Premium",
321+
ultimate: "Ultimate",
322+
}[requirement.requiredTier] ?? requirement.requiredTier;
323+
324+
return `[tier: ${tierBadge}]`;
325+
}
300326

327+
// For tool-level queries, show highest tier required by any action
328+
const highestTier = ToolAvailability.getHighestTier(toolName);
301329
const tierBadge =
302330
{
303331
free: "Free",
304332
premium: "Premium",
305333
ultimate: "Ultimate",
306-
}[requirement.requiredTier] ?? requirement.requiredTier;
334+
}[highestTier] ?? highestTier;
335+
336+
// Mark if tool has mixed tiers (default tier differs from highest tier)
337+
const toolReq = ToolAvailability.getActionRequirement(toolName);
338+
const defaultTier = toolReq?.tier ?? "free";
339+
const hasMixedTiers = highestTier !== defaultTier;
340+
341+
if (hasMixedTiers) {
342+
return `[tier: ${tierBadge}*]`;
343+
}
307344

308345
return `[tier: ${tierBadge}]`;
309346
}
@@ -793,10 +830,12 @@ function generateExportMarkdown(
793830
if (actions.length > 0) {
794831
lines.push("#### Actions");
795832
lines.push("");
796-
lines.push("| Action | Description |");
797-
lines.push("|--------|-------------|");
833+
lines.push("| Action | Tier | Description |");
834+
lines.push("|--------|------|-------------|");
798835
for (const action of actions) {
799-
lines.push(`| \`${action.name}\` | ${action.description} |`);
836+
const actionTierInfo = getToolTierInfo(tool.name, action.name);
837+
const tierDisplay = actionTierInfo.replace("[tier: ", "").replace(/]/g, "") || "Free";
838+
lines.push(`| \`${action.name}\` | ${tierDisplay} | ${action.description} |`);
800839
}
801840
lines.push("");
802841
}

src/services/ToolAvailability.ts

Lines changed: 283 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,33 @@ interface ToolRequirement {
88
notes?: string;
99
}
1010

11+
/**
12+
* Action-level tier requirement
13+
*/
14+
interface ActionRequirement {
15+
tier: "free" | "premium" | "ultimate";
16+
minVersion: number;
17+
notes?: string;
18+
}
19+
20+
/**
21+
* Tool with action-level requirements (for consolidated tools)
22+
*/
23+
interface ToolActionRequirements {
24+
/** Default requirement when action is not specified */
25+
default: ActionRequirement;
26+
/** Action-specific requirements (override default) */
27+
actions?: Record<string, ActionRequirement>;
28+
}
29+
1130
export class ToolAvailability {
31+
/** Tier hierarchy for comparison: free < premium < ultimate */
32+
private static readonly TIER_ORDER: Record<string, number> = {
33+
free: 0,
34+
premium: 1,
35+
ultimate: 2,
36+
};
37+
1238
// Comprehensive tool requirements based on GitLab documentation
1339
private static toolRequirements: Record<string, ToolRequirement> = {
1440
// Core Tools - Available in Free Tier
@@ -413,7 +439,233 @@ export class ToolAvailability {
413439
unprotect_repository_tag: { minVersion: 11.3, requiredTier: "free" },
414440
};
415441

416-
public static isToolAvailable(toolName: string): boolean {
442+
// ============================================================================
443+
// Consolidated Tools with Action-Level Requirements
444+
// ============================================================================
445+
446+
private static actionRequirements: Record<string, ToolActionRequirements> = {
447+
// Core tools
448+
browse_projects: {
449+
default: { tier: "free", minVersion: 8.0 },
450+
},
451+
browse_namespaces: {
452+
default: { tier: "free", minVersion: 9.0 },
453+
},
454+
browse_commits: {
455+
default: { tier: "free", minVersion: 8.0 },
456+
},
457+
browse_events: {
458+
default: { tier: "free", minVersion: 9.0 },
459+
},
460+
create_branch: {
461+
default: { tier: "free", minVersion: 8.0 },
462+
},
463+
create_group: {
464+
default: { tier: "free", minVersion: 8.0 },
465+
},
466+
manage_repository: {
467+
default: { tier: "free", minVersion: 8.0 },
468+
},
469+
get_users: {
470+
default: { tier: "free", minVersion: 8.0 },
471+
},
472+
473+
// Merge Requests
474+
browse_merge_requests: {
475+
default: { tier: "free", minVersion: 8.0 },
476+
actions: {
477+
approvals: { tier: "premium", minVersion: 10.6, notes: "MR approvals" },
478+
},
479+
},
480+
browse_mr_discussions: {
481+
default: { tier: "free", minVersion: 8.0 },
482+
},
483+
manage_merge_request: {
484+
default: { tier: "free", minVersion: 8.0 },
485+
},
486+
manage_mr_discussion: {
487+
default: { tier: "free", minVersion: 8.0 },
488+
actions: {
489+
comment: { tier: "free", minVersion: 8.0 },
490+
thread: { tier: "free", minVersion: 11.0 },
491+
reply: { tier: "free", minVersion: 11.0 },
492+
update: { tier: "free", minVersion: 8.0 },
493+
apply_suggestion: { tier: "free", minVersion: 13.0 },
494+
apply_suggestions: { tier: "free", minVersion: 13.0 },
495+
},
496+
},
497+
manage_draft_notes: {
498+
default: { tier: "free", minVersion: 13.2 },
499+
},
500+
501+
// Work Items
502+
browse_work_items: {
503+
default: { tier: "free", minVersion: 15.0 },
504+
},
505+
manage_work_item: {
506+
default: { tier: "free", minVersion: 15.0 },
507+
},
508+
509+
// Labels
510+
browse_labels: {
511+
default: { tier: "free", minVersion: 8.0 },
512+
},
513+
manage_label: {
514+
default: { tier: "free", minVersion: 8.0 },
515+
},
516+
517+
// Wiki
518+
browse_wiki: {
519+
default: { tier: "free", minVersion: 9.0 },
520+
},
521+
manage_wiki: {
522+
default: { tier: "free", minVersion: 9.0 },
523+
},
524+
525+
// Pipelines
526+
browse_pipelines: {
527+
default: { tier: "free", minVersion: 9.0 },
528+
},
529+
manage_pipeline: {
530+
default: { tier: "free", minVersion: 9.0 },
531+
},
532+
manage_pipeline_job: {
533+
default: { tier: "free", minVersion: 9.0 },
534+
},
535+
536+
// Variables
537+
browse_variables: {
538+
default: { tier: "free", minVersion: 9.0 },
539+
},
540+
manage_variable: {
541+
default: { tier: "free", minVersion: 9.0 },
542+
},
543+
544+
// Milestones
545+
browse_milestones: {
546+
default: { tier: "free", minVersion: 8.0 },
547+
actions: {
548+
burndown: { tier: "premium", minVersion: 12.0, notes: "Burndown charts" },
549+
},
550+
},
551+
manage_milestone: {
552+
default: { tier: "free", minVersion: 8.0 },
553+
},
554+
555+
// Files
556+
browse_files: {
557+
default: { tier: "free", minVersion: 8.0 },
558+
},
559+
manage_files: {
560+
default: { tier: "free", minVersion: 8.0 },
561+
},
562+
563+
// Snippets
564+
browse_snippets: {
565+
default: { tier: "free", minVersion: 8.15 },
566+
},
567+
manage_snippet: {
568+
default: { tier: "free", minVersion: 8.15 },
569+
},
570+
571+
// Webhooks
572+
list_webhooks: {
573+
default: { tier: "free", minVersion: 8.0, notes: "Project webhooks" },
574+
},
575+
manage_webhook: {
576+
default: { tier: "free", minVersion: 8.0, notes: "Project webhooks" },
577+
actions: {
578+
create_group: { tier: "premium", minVersion: 10.4, notes: "Group webhooks" },
579+
update_group: { tier: "premium", minVersion: 10.4, notes: "Group webhooks" },
580+
delete_group: { tier: "premium", minVersion: 10.4, notes: "Group webhooks" },
581+
},
582+
},
583+
584+
// Integrations
585+
list_integrations: {
586+
default: { tier: "free", minVersion: 8.0 },
587+
},
588+
manage_integration: {
589+
default: { tier: "free", minVersion: 8.0 },
590+
},
591+
592+
// Todos
593+
list_todos: {
594+
default: { tier: "free", minVersion: 8.0 },
595+
},
596+
manage_todos: {
597+
default: { tier: "free", minVersion: 8.0 },
598+
},
599+
600+
// Additional tools
601+
list_project_members: {
602+
default: { tier: "free", minVersion: 8.0 },
603+
},
604+
list_group_iterations: {
605+
default: { tier: "premium", minVersion: 13.1, notes: "Iterations/Sprints" },
606+
},
607+
download_attachment: {
608+
default: { tier: "free", minVersion: 10.0 },
609+
},
610+
};
611+
612+
/**
613+
* Get requirement for a tool, optionally with action
614+
*/
615+
public static getActionRequirement(
616+
toolName: string,
617+
action?: string
618+
): ActionRequirement | undefined {
619+
const toolReq = this.actionRequirements[toolName];
620+
if (!toolReq) return undefined;
621+
622+
// If action specified, check action-specific requirement first
623+
if (action && toolReq.actions?.[action]) {
624+
return toolReq.actions[action];
625+
}
626+
627+
return toolReq.default;
628+
}
629+
630+
/**
631+
* Get the highest tier required by any action of a tool
632+
*/
633+
public static getHighestTier(toolName: string): "free" | "premium" | "ultimate" {
634+
const toolReq = this.actionRequirements[toolName];
635+
if (!toolReq) {
636+
// Fallback to legacy requirements
637+
const legacy = this.toolRequirements[toolName];
638+
return legacy?.requiredTier ?? "free";
639+
}
640+
641+
let highest = toolReq.default.tier;
642+
643+
if (toolReq.actions) {
644+
for (const actionReq of Object.values(toolReq.actions)) {
645+
if (this.TIER_ORDER[actionReq.tier] > this.TIER_ORDER[highest]) {
646+
highest = actionReq.tier;
647+
}
648+
}
649+
}
650+
651+
return highest;
652+
}
653+
654+
/**
655+
* Get all actions that require a specific tier or higher
656+
*/
657+
public static getTierRestrictedActions(toolName: string, tier: "premium" | "ultimate"): string[] {
658+
const toolReq = this.actionRequirements[toolName];
659+
if (!toolReq?.actions) return [];
660+
661+
const minLevel = this.TIER_ORDER[tier];
662+
663+
return Object.entries(toolReq.actions)
664+
.filter(([, req]) => this.TIER_ORDER[req.tier] >= minLevel)
665+
.map(([action]) => action);
666+
}
667+
668+
public static isToolAvailable(toolName: string, action?: string): boolean {
417669
const connectionManager = ConnectionManager.getInstance();
418670

419671
// Add null check as extra safety
@@ -424,6 +676,18 @@ export class ToolAvailability {
424676

425677
try {
426678
const instanceInfo = connectionManager.getInstanceInfo();
679+
680+
// Check action requirements first (consolidated tools)
681+
const actionReq = this.getActionRequirement(toolName, action);
682+
if (actionReq) {
683+
const version = this.parseVersion(instanceInfo.version);
684+
if (version < actionReq.minVersion) {
685+
return false;
686+
}
687+
return this.isTierSufficient(instanceInfo.tier, actionReq.tier);
688+
}
689+
690+
// Fallback to legacy requirements
427691
const requirement = this.toolRequirements[toolName];
428692

429693
if (!requirement) {
@@ -459,10 +723,26 @@ export class ToolAvailability {
459723
}
460724

461725
public static getAvailableTools(): string[] {
462-
return Object.keys(this.toolRequirements).filter(tool => this.isToolAvailable(tool));
726+
// Combine tools from both legacy toolRequirements and new actionRequirements
727+
const allTools = new Set([
728+
...Object.keys(this.toolRequirements),
729+
...Object.keys(this.actionRequirements),
730+
]);
731+
return Array.from(allTools).filter(tool => this.isToolAvailable(tool));
463732
}
464733

465-
public static getToolRequirement(toolName: string): ToolRequirement | undefined {
734+
public static getToolRequirement(toolName: string, action?: string): ToolRequirement | undefined {
735+
// Check action requirements first (consolidated tools)
736+
const actionReq = this.getActionRequirement(toolName, action);
737+
if (actionReq) {
738+
return {
739+
minVersion: actionReq.minVersion,
740+
requiredTier: actionReq.tier,
741+
notes: actionReq.notes,
742+
};
743+
}
744+
745+
// Fallback to legacy requirements
466746
return this.toolRequirements[toolName];
467747
}
468748

0 commit comments

Comments
 (0)