Skip to content

Commit 578be93

Browse files
authored
Merge pull request #36 from structured-world/refactor/#10-wiki-cqrs-consolidation
feat(wiki): consolidate 5 wiki tools into 2 CQRS tools
2 parents 6c44e64 + 0205f20 commit 578be93

File tree

5 files changed

+897
-693
lines changed

5 files changed

+897
-693
lines changed

src/entities/wiki/registry.ts

Lines changed: 90 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,112 +1,127 @@
11
import * as z from "zod";
2-
import { ListWikiPagesSchema, GetWikiPageSchema } from "./schema-readonly";
3-
import { CreateWikiPageSchema, UpdateWikiPageSchema, DeleteWikiPageSchema } from "./schema";
2+
import { BrowseWikiSchema } from "./schema-readonly";
3+
import { ManageWikiSchema } from "./schema";
44
import { gitlab, toQuery } from "../../utils/gitlab-api";
55
import { resolveNamespaceForAPI } from "../../utils/namespace";
66
import { ToolRegistry, EnhancedToolDefinition } from "../../types";
7+
import { assertDefined } from "../utils";
78

89
/**
9-
* Wiki tools registry - unified registry containing all wiki operation tools with their handlers
10+
* Wiki tools registry - 2 CQRS tools replacing 5 individual tools
11+
*
12+
* browse_wiki (Query): list, get
13+
* manage_wiki (Command): create, update, delete
1014
*/
1115
export const wikiToolRegistry: ToolRegistry = new Map<string, EnhancedToolDefinition>([
16+
// ============================================================================
17+
// browse_wiki - CQRS Query Tool
18+
// ============================================================================
1219
[
13-
"list_wiki_pages",
20+
"browse_wiki",
1421
{
15-
name: "list_wiki_pages",
22+
name: "browse_wiki",
1623
description:
17-
"BROWSE: Explore all wiki pages in project or group documentation. Use when: Discovering available guides and documentation, Understanding project knowledge base structure, Finding existing pages before creating new ones. Wiki provides collaborative documentation separate from code repository. Returns page titles, slugs, content formats, and creation dates.",
18-
inputSchema: z.toJSONSchema(ListWikiPagesSchema),
24+
'BROWSE wiki pages. Actions: "list" shows all wiki pages in project/group, "get" retrieves single wiki page content by slug.',
25+
inputSchema: z.toJSONSchema(BrowseWikiSchema),
1926
handler: async (args: unknown) => {
20-
const options = ListWikiPagesSchema.parse(args);
21-
const { entityType, encodedPath } = await resolveNamespaceForAPI(options.namespace);
27+
const input = BrowseWikiSchema.parse(args);
28+
const { entityType, encodedPath } = await resolveNamespaceForAPI(input.namespace);
2229

23-
return gitlab.get(`${entityType}/${encodedPath}/wikis`, {
24-
query: toQuery(options, ["namespace"]),
25-
});
26-
},
27-
},
28-
],
29-
[
30-
"get_wiki_page",
31-
{
32-
name: "get_wiki_page",
33-
description:
34-
"READ: Get complete wiki page content and metadata by slug. Use when: Reading technical documentation and guides, Accessing project knowledge base content, Getting full markdown with formatting. Returns complete page content, metadata, edit history, and author information. Perfect for content analysis and documentation review.",
35-
inputSchema: z.toJSONSchema(GetWikiPageSchema),
36-
handler: async (args: unknown) => {
37-
const options = GetWikiPageSchema.parse(args);
38-
const { entityType, encodedPath } = await resolveNamespaceForAPI(options.namespace);
30+
switch (input.action) {
31+
case "list": {
32+
const { action: _action, namespace: _namespace, ...rest } = input;
33+
const query = toQuery(rest, []);
3934

40-
return gitlab.get(`${entityType}/${encodedPath}/wikis/${encodeURIComponent(options.slug)}`);
41-
},
42-
},
43-
],
44-
[
45-
"create_wiki_page",
46-
{
47-
name: "create_wiki_page",
48-
description:
49-
"CREATE: Add new documentation page to project or group wiki. Use when: Adding technical documentation, user guides, or FAQs, Creating project knowledge base content, Establishing team documentation standards. Check list_wiki_pages FIRST to avoid duplicate topics. Supports GitLab Flavored Markdown with extensions. Creates version-controlled documentation.",
50-
inputSchema: z.toJSONSchema(CreateWikiPageSchema),
51-
handler: async (args: unknown) => {
52-
const options = CreateWikiPageSchema.parse(args);
53-
const { entityType, encodedPath } = await resolveNamespaceForAPI(options.namespace);
54-
const { namespace: _namespace, ...body } = options;
55-
56-
return gitlab.post(`${entityType}/${encodedPath}/wikis`, {
57-
body,
58-
contentType: "json",
59-
});
60-
},
61-
},
62-
],
63-
[
64-
"update_wiki_page",
65-
{
66-
name: "update_wiki_page",
67-
description:
68-
"UPDATE: Modify existing wiki page content or properties. Use when: Updating documentation with new information, Fixing errors or improving clarity, Reorganizing content structure. Maintains complete version history with change tracking. Supports collaborative editing with author attribution and diff viewing.",
69-
inputSchema: z.toJSONSchema(UpdateWikiPageSchema),
70-
handler: async (args: unknown) => {
71-
const options = UpdateWikiPageSchema.parse(args);
72-
const { entityType, encodedPath } = await resolveNamespaceForAPI(options.namespace);
73-
const { namespace: _namespace, slug, ...body } = options;
74-
75-
return gitlab.put(`${entityType}/${encodedPath}/wikis/${encodeURIComponent(slug)}`, {
76-
body,
77-
contentType: "json",
78-
});
35+
return gitlab.get(`${entityType}/${encodedPath}/wikis`, { query });
36+
}
37+
38+
case "get": {
39+
// slug is required for get action (validated by .refine())
40+
assertDefined(input.slug, "slug");
41+
return gitlab.get(
42+
`${entityType}/${encodedPath}/wikis/${encodeURIComponent(input.slug)}`
43+
);
44+
}
45+
46+
/* istanbul ignore next -- unreachable with Zod validation */
47+
default:
48+
throw new Error(`Unknown action: ${(input as { action: string }).action}`);
49+
}
7950
},
8051
},
8152
],
53+
54+
// ============================================================================
55+
// manage_wiki - CQRS Command Tool
56+
// ============================================================================
8257
[
83-
"delete_wiki_page",
58+
"manage_wiki",
8459
{
85-
name: "delete_wiki_page",
60+
name: "manage_wiki",
8661
description:
87-
"DELETE: Permanently remove wiki page from documentation. Use when: Cleaning up outdated or obsolete content, Removing duplicate or incorrect pages, Reorganizing wiki structure. WARNING: Deletes page and ALL version history permanently - cannot be undone. Consider archiving important content first.",
88-
inputSchema: z.toJSONSchema(DeleteWikiPageSchema),
62+
'MANAGE wiki pages. Actions: "create" adds new wiki page, "update" modifies existing page, "delete" removes wiki page permanently.',
63+
inputSchema: z.toJSONSchema(ManageWikiSchema),
8964
handler: async (args: unknown) => {
90-
const options = DeleteWikiPageSchema.parse(args);
91-
const { entityType, encodedPath } = await resolveNamespaceForAPI(options.namespace);
65+
const input = ManageWikiSchema.parse(args);
66+
const { entityType, encodedPath } = await resolveNamespaceForAPI(input.namespace);
67+
68+
switch (input.action) {
69+
case "create": {
70+
const { action: _action, namespace: _namespace, ...body } = input;
71+
72+
return gitlab.post(`${entityType}/${encodedPath}/wikis`, {
73+
body,
74+
contentType: "json",
75+
});
76+
}
77+
78+
case "update": {
79+
// slug is required for update action (validated by .refine())
80+
assertDefined(input.slug, "slug");
81+
const { action: _action, namespace: _namespace, slug, ...body } = input;
9282

93-
await gitlab.delete(
94-
`${entityType}/${encodedPath}/wikis/${encodeURIComponent(options.slug)}`
95-
);
96-
return { deleted: true };
83+
return gitlab.put(`${entityType}/${encodedPath}/wikis/${encodeURIComponent(slug)}`, {
84+
body,
85+
contentType: "json",
86+
});
87+
}
88+
89+
case "delete": {
90+
// slug is required for delete action (validated by .refine())
91+
assertDefined(input.slug, "slug");
92+
93+
await gitlab.delete(
94+
`${entityType}/${encodedPath}/wikis/${encodeURIComponent(input.slug)}`
95+
);
96+
return { deleted: true };
97+
}
98+
99+
/* istanbul ignore next -- unreachable with Zod validation */
100+
default:
101+
throw new Error(`Unknown action: ${(input as { action: string }).action}`);
102+
}
97103
},
98104
},
99105
],
100106
]);
101107

108+
/**
109+
* Get read-only tool names from the registry
110+
*/
102111
export function getWikiReadOnlyToolNames(): string[] {
103-
return ["list_wiki_pages", "get_wiki_page"];
112+
return ["browse_wiki"];
104113
}
105114

115+
/**
116+
* Get all tool definitions from the registry
117+
*/
106118
export function getWikiToolDefinitions(): EnhancedToolDefinition[] {
107119
return Array.from(wikiToolRegistry.values());
108120
}
109121

122+
/**
123+
* Get filtered tools based on read-only mode
124+
*/
110125
export function getFilteredWikiTools(readOnlyMode: boolean = false): EnhancedToolDefinition[] {
111126
if (readOnlyMode) {
112127
const readOnlyNames = getWikiReadOnlyToolNames();
Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,39 @@
11
import { z } from "zod";
2-
import { PaginationOptionsSchema } from "../shared";
32
import { flexibleBoolean } from "../utils";
43

5-
// Read-only wiki operation schemas
6-
export const ListWikiPagesSchema = z
4+
// ============================================================================
5+
// browse_wiki - CQRS Query Tool (flat schema for Claude API compatibility)
6+
// Actions: list, get
7+
// NOTE: Uses flat z.object() with .refine() instead of z.discriminatedUnion()
8+
// because Claude API doesn't support oneOf/allOf/anyOf at JSON Schema root level.
9+
// ============================================================================
10+
11+
export const BrowseWikiSchema = z
712
.object({
8-
namespace: z.string().describe("Namespace path (group or project) to list wiki pages from"),
9-
with_content: flexibleBoolean.optional().describe("Include content of the wiki pages"),
13+
action: z.enum(["list", "get"]).describe("Action to perform"),
14+
namespace: z.string().describe("Namespace path (group or project)"),
15+
// get action fields
16+
slug: z
17+
.string()
18+
.optional()
19+
.describe("URL-encoded slug of the wiki page. Required for 'get' action."),
20+
// list action fields
21+
with_content: flexibleBoolean
22+
.optional()
23+
.describe("For 'list': include content of the wiki pages"),
24+
// pagination fields (for list)
25+
per_page: z.number().optional().describe("Number of items per page"),
26+
page: z.number().optional().describe("Page number"),
1027
})
11-
.merge(PaginationOptionsSchema);
28+
.refine(data => data.action !== "get" || data.slug !== undefined, {
29+
message: "slug is required for 'get' action",
30+
path: ["slug"],
31+
});
1232

13-
export const GetWikiPageSchema = z.object({
14-
namespace: z.string().describe("Namespace path (group or project) containing the wiki page"),
15-
slug: z.string().describe("URL-encoded slug of the wiki page"),
16-
});
33+
// ============================================================================
34+
// Response schemas for wiki pages
35+
// ============================================================================
1736

18-
// Define wiki response schemas
1937
export const GitLabWikiPageSchema = z.object({
2038
title: z.string(),
2139
slug: z.string(),
@@ -25,7 +43,9 @@ export const GitLabWikiPageSchema = z.object({
2543
updated_at: z.string().optional(),
2644
});
2745

46+
// ============================================================================
2847
// Type exports
29-
export type ListWikiPagesOptions = z.infer<typeof ListWikiPagesSchema>;
30-
export type GetWikiPageOptions = z.infer<typeof GetWikiPageSchema>;
48+
// ============================================================================
49+
50+
export type BrowseWikiInput = z.infer<typeof BrowseWikiSchema>;
3151
export type GitLabWikiPage = z.infer<typeof GitLabWikiPageSchema>;

src/entities/wiki/schema.ts

Lines changed: 42 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,47 @@
11
import { z } from "zod";
22

3-
// Write-only wiki operation schemas
4-
export const CreateWikiPageSchema = z.object({
5-
namespace: z.string().describe("Namespace path (group or project) to create wiki page in"),
6-
title: z.string().describe("Title of the wiki page"),
7-
content: z.string().describe("Content of the wiki page"),
8-
format: z.string().optional().describe("Content format, e.g., markdown, rdoc"),
9-
});
3+
// ============================================================================
4+
// manage_wiki - CQRS Command Tool (flat schema for Claude API compatibility)
5+
// Actions: create, update, delete
6+
// NOTE: Uses flat z.object() with .refine() instead of z.discriminatedUnion()
7+
// because Claude API doesn't support oneOf/allOf/anyOf at JSON Schema root level.
8+
// ============================================================================
109

11-
export const UpdateWikiPageSchema = z.object({
12-
namespace: z.string().describe("Namespace path (group or project) containing the wiki page"),
13-
slug: z.string().describe("URL-encoded slug of the wiki page"),
14-
title: z.string().optional().describe("New title of the wiki page"),
15-
content: z.string().optional().describe("New content of the wiki page"),
16-
format: z.string().optional().describe("Content format, e.g., markdown, rdoc"),
17-
});
18-
19-
export const DeleteWikiPageSchema = z.object({
20-
namespace: z.string().describe("Namespace path (group or project) containing the wiki page"),
21-
slug: z.string().describe("URL-encoded slug of the wiki page"),
22-
});
10+
export const ManageWikiSchema = z
11+
.object({
12+
action: z.enum(["create", "update", "delete"]).describe("Action to perform"),
13+
namespace: z.string().describe("Namespace path (group or project)"),
14+
// update/delete action fields
15+
slug: z
16+
.string()
17+
.optional()
18+
.describe("URL-encoded slug of the wiki page. Required for 'update' and 'delete' actions."),
19+
// create/update action fields
20+
title: z.string().optional().describe("Title of the wiki page. Required for 'create' action."),
21+
content: z
22+
.string()
23+
.optional()
24+
.describe("Content of the wiki page. Required for 'create' action."),
25+
format: z
26+
.enum(["markdown", "rdoc", "asciidoc", "org"])
27+
.optional()
28+
.describe("Content format (markdown, rdoc, asciidoc, org). Defaults to markdown."),
29+
})
30+
.refine(data => data.action === "create" || data.slug !== undefined, {
31+
message: "slug is required for 'update' and 'delete' actions",
32+
path: ["slug"],
33+
})
34+
.refine(data => data.action !== "create" || data.title !== undefined, {
35+
message: "title is required for 'create' action",
36+
path: ["title"],
37+
})
38+
.refine(data => data.action !== "create" || data.content !== undefined, {
39+
message: "content is required for 'create' action",
40+
path: ["content"],
41+
});
2342

43+
// ============================================================================
2444
// Type exports
25-
export type CreateWikiPageOptions = z.infer<typeof CreateWikiPageSchema>;
26-
export type UpdateWikiPageOptions = z.infer<typeof UpdateWikiPageSchema>;
27-
export type DeleteWikiPageOptions = z.infer<typeof DeleteWikiPageSchema>;
45+
// ============================================================================
46+
47+
export type ManageWikiInput = z.infer<typeof ManageWikiSchema>;

0 commit comments

Comments
 (0)