|
1 | 1 | import * as z from "zod"; |
2 | | -import { ListLabelsSchema, GetLabelSchema } from "./schema-readonly"; |
3 | | -import { CreateLabelSchema, UpdateLabelSchema, DeleteLabelSchema } from "./schema"; |
| 2 | +import { BrowseLabelsSchema } from "./schema-readonly"; |
| 3 | +import { ManageLabelSchema } from "./schema"; |
4 | 4 | import { gitlab, toQuery } from "../../utils/gitlab-api"; |
5 | 5 | import { resolveNamespaceForAPI } from "../../utils/namespace"; |
6 | 6 | import { ToolRegistry, EnhancedToolDefinition } from "../../types"; |
| 7 | +import { assertDefined } from "../utils"; |
7 | 8 |
|
8 | 9 | /** |
9 | | - * Labels tools registry - unified registry containing all labels tools with their handlers |
| 10 | + * Labels tools registry - 2 CQRS tools replacing 5 individual tools |
| 11 | + * |
| 12 | + * browse_labels (Query): list, get |
| 13 | + * manage_label (Command): create, update, delete |
10 | 14 | */ |
11 | 15 | export const labelsToolRegistry: ToolRegistry = new Map<string, EnhancedToolDefinition>([ |
| 16 | + // ============================================================================ |
| 17 | + // browse_labels - CQRS Query Tool |
| 18 | + // ============================================================================ |
12 | 19 | [ |
13 | | - "list_labels", |
| 20 | + "browse_labels", |
14 | 21 | { |
15 | | - name: "list_labels", |
| 22 | + name: "browse_labels", |
16 | 23 | description: |
17 | | - "DISCOVER FIRST: Browse all existing labels in a project or group - RUN THIS BEFORE creating new labels! Use when: Choosing labels for issues/MRs, Understanding established taxonomy, Avoiding duplicate label creation. Returns label names, colors, descriptions, and priorities. Group labels are inherited by all projects. See also: create_label (only after checking existing labels).", |
18 | | - inputSchema: z.toJSONSchema(ListLabelsSchema), |
| 24 | + 'BROWSE labels. Actions: "list" shows all labels in project/group with filtering, "get" retrieves single label details by ID or name.', |
| 25 | + inputSchema: z.toJSONSchema(BrowseLabelsSchema), |
19 | 26 | handler: async (args: unknown) => { |
20 | | - const options = ListLabelsSchema.parse(args); |
21 | | - const { namespace } = options; |
22 | | - const entityType = namespace.includes("/") ? "projects" : "groups"; |
| 27 | + const input = BrowseLabelsSchema.parse(args); |
| 28 | + const { entityType, encodedPath } = await resolveNamespaceForAPI(input.namespace); |
23 | 29 |
|
24 | | - return gitlab.get(`${entityType}/${encodeURIComponent(namespace)}/labels`, { |
25 | | - query: toQuery(options, ["namespace"]), |
26 | | - }); |
27 | | - }, |
28 | | - }, |
29 | | - ], |
30 | | - [ |
31 | | - "get_label", |
32 | | - { |
33 | | - name: "get_label", |
34 | | - description: |
35 | | - "READ: Retrieve details of a specific label by ID or name. Use when: Getting full label information including color and description, Checking label usage statistics, Validating label properties. Works for both project-specific and group-inherited labels. See also: list_labels to browse all available labels first.", |
36 | | - inputSchema: z.toJSONSchema(GetLabelSchema), |
37 | | - handler: async (args: unknown) => { |
38 | | - const options = GetLabelSchema.parse(args); |
39 | | - const { entityType, encodedPath } = await resolveNamespaceForAPI(options.namespace); |
| 30 | + switch (input.action) { |
| 31 | + case "list": { |
| 32 | + const { action: _action, namespace: _namespace, label_id: _labelId, ...rest } = input; |
| 33 | + const query = toQuery(rest, []); |
40 | 34 |
|
41 | | - return gitlab.get( |
42 | | - `${entityType}/${encodedPath}/labels/${encodeURIComponent(options.label_id)}` |
43 | | - ); |
44 | | - }, |
45 | | - }, |
46 | | - ], |
47 | | - [ |
48 | | - "create_label", |
49 | | - { |
50 | | - name: "create_label", |
51 | | - description: |
52 | | - "CREATE CAREFULLY: Add a new label ONLY after running list_labels to check existing taxonomy! Use when: Existing labels do not fit your needs, Establishing new project taxonomy. AVOID: Creating duplicates of existing labels with slight variations. Requires name and color (hex format like #FF0000). Group labels automatically become available to all child projects. See also: list_labels (run first to discover existing labels).", |
53 | | - inputSchema: z.toJSONSchema(CreateLabelSchema), |
54 | | - handler: async (args: unknown) => { |
55 | | - const options = CreateLabelSchema.parse(args); |
56 | | - const { entityType, encodedPath } = await resolveNamespaceForAPI(options.namespace); |
57 | | - |
58 | | - return gitlab.post(`${entityType}/${encodedPath}/labels`, { |
59 | | - body: { |
60 | | - name: options.name, |
61 | | - color: options.color, |
62 | | - description: options.description, |
63 | | - priority: options.priority, |
64 | | - }, |
65 | | - }); |
66 | | - }, |
67 | | - }, |
68 | | - ], |
69 | | - [ |
70 | | - "update_label", |
71 | | - { |
72 | | - name: "update_label", |
73 | | - description: |
74 | | - "UPDATE: Modify label properties including name, color, description, or priority. Use when: Refining categorization system, Updating label appearance, Standardizing label naming. Changes apply immediately to all tagged items. Renaming updates all existing references automatically. See also: list_labels to understand current taxonomy before changes.", |
75 | | - inputSchema: z.toJSONSchema(UpdateLabelSchema), |
76 | | - handler: async (args: unknown) => { |
77 | | - const options = UpdateLabelSchema.parse(args); |
78 | | - const { entityType, encodedPath } = await resolveNamespaceForAPI(options.namespace); |
| 35 | + return gitlab.get(`${entityType}/${encodedPath}/labels`, { query }); |
| 36 | + } |
| 37 | + |
| 38 | + case "get": { |
| 39 | + // label_id is required for get action (validated by .refine()) |
| 40 | + assertDefined(input.label_id, "label_id"); |
| 41 | + const query = input.include_ancestor_groups |
| 42 | + ? toQuery({ include_ancestor_groups: input.include_ancestor_groups }, []) |
| 43 | + : undefined; |
| 44 | + |
| 45 | + return gitlab.get( |
| 46 | + `${entityType}/${encodedPath}/labels/${encodeURIComponent(input.label_id)}`, |
| 47 | + { query } |
| 48 | + ); |
| 49 | + } |
79 | 50 |
|
80 | | - const { namespace: _namespace, label_id, ...body } = options; |
81 | | - return gitlab.put(`${entityType}/${encodedPath}/labels/${encodeURIComponent(label_id)}`, { |
82 | | - body, |
83 | | - }); |
| 51 | + /* istanbul ignore next -- unreachable with Zod validation */ |
| 52 | + default: |
| 53 | + throw new Error(`Unknown action: ${(input as { action: string }).action}`); |
| 54 | + } |
84 | 55 | }, |
85 | 56 | }, |
86 | 57 | ], |
| 58 | + |
| 59 | + // ============================================================================ |
| 60 | + // manage_label - CQRS Command Tool |
| 61 | + // ============================================================================ |
87 | 62 | [ |
88 | | - "delete_label", |
| 63 | + "manage_label", |
89 | 64 | { |
90 | | - name: "delete_label", |
| 65 | + name: "manage_label", |
91 | 66 | description: |
92 | | - "DELETE: Remove a label permanently from project or group. Use when: Cleaning up unused labels, Reorganizing taxonomy. WARNING: Removes label from all issues and MRs without replacement. Consider updating items before deletion. Cannot be undone. See also: list_labels to check label usage before deletion.", |
93 | | - inputSchema: z.toJSONSchema(DeleteLabelSchema), |
| 67 | + 'MANAGE labels. Actions: "create" adds new label (requires name and color), "update" modifies existing label, "delete" removes label permanently.', |
| 68 | + inputSchema: z.toJSONSchema(ManageLabelSchema), |
94 | 69 | handler: async (args: unknown) => { |
95 | | - const options = DeleteLabelSchema.parse(args); |
96 | | - const { entityType, encodedPath } = await resolveNamespaceForAPI(options.namespace); |
| 70 | + const input = ManageLabelSchema.parse(args); |
| 71 | + const { entityType, encodedPath } = await resolveNamespaceForAPI(input.namespace); |
| 72 | + |
| 73 | + switch (input.action) { |
| 74 | + case "create": { |
| 75 | + // name and color are required for create action (validated by .refine()) |
| 76 | + assertDefined(input.name, "name"); |
| 77 | + assertDefined(input.color, "color"); |
| 78 | + |
| 79 | + return gitlab.post(`${entityType}/${encodedPath}/labels`, { |
| 80 | + body: { |
| 81 | + name: input.name, |
| 82 | + color: input.color, |
| 83 | + description: input.description, |
| 84 | + priority: input.priority, |
| 85 | + }, |
| 86 | + contentType: "json", |
| 87 | + }); |
| 88 | + } |
| 89 | + |
| 90 | + case "update": { |
| 91 | + // label_id is required for update action (validated by .refine()) |
| 92 | + assertDefined(input.label_id, "label_id"); |
| 93 | + const { |
| 94 | + action: _action, |
| 95 | + namespace: _namespace, |
| 96 | + label_id, |
| 97 | + name: _name, |
| 98 | + ...body |
| 99 | + } = input; |
97 | 100 |
|
98 | | - await gitlab.delete( |
99 | | - `${entityType}/${encodedPath}/labels/${encodeURIComponent(options.label_id)}` |
100 | | - ); |
101 | | - return { success: true, message: "Label deleted successfully" }; |
| 101 | + return gitlab.put( |
| 102 | + `${entityType}/${encodedPath}/labels/${encodeURIComponent(label_id)}`, |
| 103 | + { body, contentType: "json" } |
| 104 | + ); |
| 105 | + } |
| 106 | + |
| 107 | + case "delete": { |
| 108 | + // label_id is required for delete action (validated by .refine()) |
| 109 | + assertDefined(input.label_id, "label_id"); |
| 110 | + |
| 111 | + await gitlab.delete( |
| 112 | + `${entityType}/${encodedPath}/labels/${encodeURIComponent(input.label_id)}` |
| 113 | + ); |
| 114 | + return { deleted: true }; |
| 115 | + } |
| 116 | + |
| 117 | + /* istanbul ignore next -- unreachable with Zod validation */ |
| 118 | + default: |
| 119 | + throw new Error(`Unknown action: ${(input as { action: string }).action}`); |
| 120 | + } |
102 | 121 | }, |
103 | 122 | }, |
104 | 123 | ], |
105 | 124 | ]); |
106 | 125 |
|
| 126 | +/** |
| 127 | + * Get read-only tool names from the registry |
| 128 | + */ |
107 | 129 | export function getLabelsReadOnlyToolNames(): string[] { |
108 | | - return ["list_labels", "get_label"]; |
| 130 | + return ["browse_labels"]; |
109 | 131 | } |
110 | 132 |
|
| 133 | +/** |
| 134 | + * Get all tool definitions from the registry |
| 135 | + */ |
111 | 136 | export function getLabelsToolDefinitions(): EnhancedToolDefinition[] { |
112 | 137 | return Array.from(labelsToolRegistry.values()); |
113 | 138 | } |
114 | 139 |
|
| 140 | +/** |
| 141 | + * Get filtered tools based on read-only mode |
| 142 | + */ |
115 | 143 | export function getFilteredLabelsTools(readOnlyMode: boolean = false): EnhancedToolDefinition[] { |
116 | 144 | if (readOnlyMode) { |
117 | 145 | const readOnlyNames = getLabelsReadOnlyToolNames(); |
|
0 commit comments