Sfoglia il codice sorgente

feat: code review fixes + Wiki.js 2.5 API coverage

- Fix destructive edit_page: partial metadata updates now preserve existing fields
- edit/create_page return page id/path/locale/url for verification
- search_pages includes id; get_page_by_id coerces string ids
- get_all_pages exposes limit/locale/tags filters
- buildUrl uses configurable default locale + URL encoding
- Timeout (30s) on GraphQL client; config decoupled from mcp layer
- NodeNext module resolution; --version flag; smoke test fixed
- Add read tools: page history/version, page tree, tags, page links
- Add gated write tools: move, delete, restore version, convert editor
- Extend GraphQL queries with render/toc/contentType/publish fields
Paul Klumpp 2 giorni fa
parent
commit
56e9bcc2ce

+ 3 - 1
.github/workflows/ci.yml

@@ -47,4 +47,6 @@ jobs:
     - name: Test built executable
       run: |
         chmod +x build/index.js
-        node build/index.js --version || true
+        out=$(WIKIJS_URL=http://localhost:3000 WIKIJS_API_KEY=test node build/index.js --version)
+        echo "$out"
+        test "$out" = "$(node -p "require('./package.json').version")"

+ 12 - 3
CLAUDE.md

@@ -66,9 +66,18 @@ The project uses TypeScript with strict typing:
 
 ## MCP Tools Available
 
+Read (always available):
 - `search_pages` - Full-text search across WikiJS pages
 - `get_page_by_id` - Retrieve specific page by numeric ID
 - `get_page_by_path` - Retrieve page by path and locale
-- `get_all_pages` - List all pages with optional filters
-- `create_page` - Create a new WikiJS page (only available when WIKIJS_ENABLE_EDIT=true)
-- `edit_page` - Edit a WikiJS page by ID (only available when WIKIJS_ENABLE_EDIT=true)
+- `get_all_pages` - List pages with optional filters (limit/locale/tags)
+- `get_page_history` - Revision history of a page
+- `get_page_version` - A specific version of a page
+- `get_page_tree` - Page tree of a site
+- `list_tags` / `search_tags` - Tags
+- `get_page_links` - Page link graph for a locale
+
+Write (only available when WIKIJS_ENABLE_EDIT=true):
+- `create_page` - Create a new WikiJS page
+- `edit_page` - Edit a WikiJS page by ID (full-document replace; metadata preserved when omitted)
+- `move_page`, `delete_page`, `restore_page_version`, `convert_page_editor`

+ 2 - 1
env.example

@@ -1,3 +1,4 @@
 WIKIJS_URL=http://localhost:3000
 WIKIJS_API_KEY=
-WIKIJS_ENABLE_EDIT=false 
+WIKIJS_ENABLE_EDIT=false
+WIKIJS_DEFAULT_LOCALE=en

+ 4 - 1
package.json

@@ -4,6 +4,9 @@
 	"description": "WikiJS MCP Server",
 	"main": "build/index.js",
 	"type": "module",
+	"engines": {
+		"node": ">=18"
+	},
 	"author": "Ricardo Cenci Fabris",
 	"license": "MIT",
 	"repository": {
@@ -24,7 +27,7 @@
 		"wikijs-mcp-edit": "build/index.js"
 	},
 	"scripts": {
-		"build": "tsc && chmod 755 build/index.js",
+		"build": "tsc",
 		"codegen": "dotenv -- graphql-codegen --config codegen.yml",
 		"lint": "eslint src --ext .ts",
 		"lint:fix": "eslint src --ext .ts --fix",

+ 15 - 7
src/__tests__/version.test.ts

@@ -1,11 +1,19 @@
-import { describe, it, expect } from '@jest/globals';
-import { getVersion } from '../util/version.js';
+import { describe, it, expect, beforeEach } from '@jest/globals';
+import { readFileSync } from 'fs';
+import { fileURLToPath } from 'url';
+import { dirname, join } from 'path';
+import { getVersion, __resetVersionCache } from '../util/version.js';
 
 describe('getVersion', () => {
-  it('should return a version string', () => {
-    const version = getVersion();
-    expect(version).toBeDefined();
-    expect(typeof version).toBe('string');
-    expect(version).toMatch(/^\d+\.\d+\.\d+$/);
+  beforeEach(() => __resetVersionCache());
+
+  it('returns the version declared in package.json', () => {
+    const __dirname = dirname(fileURLToPath(import.meta.url));
+    const pkg = JSON.parse(readFileSync(join(__dirname, '../../package.json'), 'utf8')) as { version: string };
+    expect(getVersion()).toBe(pkg.version);
+  });
+
+  it('returns a string', () => {
+    expect(typeof getVersion()).toBe('string');
   });
 });

+ 24 - 10
src/config/index.ts

@@ -1,17 +1,31 @@
+export interface WikiConfig {
+  WIKIJS_URL: string;
+  WIKIJS_API_KEY: string;
+  ENABLE_EDIT: boolean;
+  DEFAULT_LOCALE: string;
+}
+
+export function loadConfig(env: NodeJS.ProcessEnv = process.env): WikiConfig | { error: string } {
+  const url = env.WIKIJS_URL;
+  const key = env.WIKIJS_API_KEY;
+  if (!url || !key) {
+    return { error: 'WIKIJS_URL and WIKIJS_API_KEY must be set (via environment or .env file)' };
+  }
+  return {
+    WIKIJS_URL: url.replace(/\/+$/, ''),
+    WIKIJS_API_KEY: key,
+    ENABLE_EDIT: env.WIKIJS_ENABLE_EDIT === 'true',
+    DEFAULT_LOCALE: env.WIKIJS_DEFAULT_LOCALE || 'en'
+  };
+}
+
+// Backwards-compatible module-level config (throws at import only when consumed).
 import path from 'path';
 import { config as configDotenv } from 'dotenv';
 import { fileURLToPath } from 'url';
 
 configDotenv({
-	path: path.join(path.dirname(fileURLToPath(import.meta.url)), '../../.env'),
+  path: path.join(path.dirname(fileURLToPath(import.meta.url)), '../../.env'),
 });
 
-if (!process.env.WIKIJS_URL || !process.env.WIKIJS_API_KEY) {
-	throw new Error('WIKIJS_URL and WIKIJS_API_KEY must be set in .env file');
-}
-
-export const CONFIG = {
-	WIKIJS_URL: process.env.WIKIJS_URL,
-	WIKIJS_API_KEY: process.env.WIKIJS_API_KEY,
-	ENABLE_EDIT: process.env.WIKIJS_ENABLE_EDIT === 'true' || false
-};
+export const CONFIG = loadConfig();

+ 31 - 8
src/index.ts

@@ -7,14 +7,27 @@ import { WikiJSMcpServer } from './mcp/index.js';
 import { getVersion } from './util/version.js';
 
 async function main() {
-	const version = getVersion();	
+	const version = getVersion();
+
+	if (process.argv.includes('--version') || process.argv.includes('-v')) {
+		process.stdout.write(`${version}\n`);
+		return;
+	}
+
+	if ('error' in CONFIG) {
+		console.error(CONFIG.error);
+		process.exit(1);
+	}
+	const cfg = CONFIG;
+
 	console.error(`Version: ${version}`);
 
-	const wikiClient = new WikiJSClient(CONFIG.WIKIJS_URL, CONFIG.WIKIJS_API_KEY);
-	
+	const wikiClient = new WikiJSClient(cfg.WIKIJS_URL, cfg.WIKIJS_API_KEY, cfg.DEFAULT_LOCALE);
+		
 	const server = new WikiJSMcpServer(wikiClient, {
 		version,
-		description: 'You are a helpful assistant that can search for pages in WikiJS by query string, get a WikiJS page by its ID, and get a WikiJS page by its path and locale.'
+		description: 'You are a helpful assistant that can search for, read, create and edit pages in WikiJS. Prefer search_pages then get_page_by_id/get_page_by_path to read full content before editing. edit_page replaces the whole document: pass the complete new content.',
+		enableEdit: cfg.ENABLE_EDIT
 	});
 
 	const transport = new StdioServerTransport();
@@ -22,7 +35,17 @@ async function main() {
 	console.error('WikiJS MCP server running on stdio');
 }
 
-main().catch((error) => {
-	console.error('Fatal error in main():', error);
-	process.exit(1);
-});
+const isMain = (() => {
+	try {
+		return import.meta.url === new URL(process.argv[1], 'file:').href;
+	} catch {
+		return true;
+	}
+})();
+
+if (isMain) {
+	main().catch((error) => {
+		console.error('Fatal error in main():', error);
+		process.exit(1);
+	});
+}

+ 29 - 5
src/mcp/index.ts

@@ -5,17 +5,28 @@ import { createTool as createGetPageByPathTool, PARAMETERS as GET_PAGE_BY_PATH_T
 import { createTool as createGetAllPagesTool, PARAMETERS as GET_ALL_PAGES_TOOL_PARAMETERS } from './tools/getAllPages.js';
 import { createTool as createEditPageTool, PARAMETERS as EDIT_PAGE_TOOL_PARAMETERS } from './tools/editPage.js';
 import { createTool as createCreatePageTool, PARAMETERS as CREATE_PAGE_TOOL_PARAMETERS } from './tools/createPage.js';
-import { WikiJSClient } from '../wikijs';
+import { createTool as createGetPageHistoryTool, PARAMETERS as GET_PAGE_HISTORY_TOOL_PARAMETERS } from './tools/getPageHistory.js';
+import { createTool as createGetPageVersionTool, PARAMETERS as GET_PAGE_VERSION_TOOL_PARAMETERS } from './tools/getPageVersion.js';
+import { createTool as createGetPageTreeTool, PARAMETERS as GET_PAGE_TREE_TOOL_PARAMETERS } from './tools/getPageTree.js';
+import { createTool as createListTagsTool, PARAMETERS as LIST_TAGS_TOOL_PARAMETERS } from './tools/listTags.js';
+import { createTool as createSearchTagsTool, PARAMETERS as SEARCH_TAGS_TOOL_PARAMETERS } from './tools/searchTags.js';
+import { createTool as createGetPageLinksTool, PARAMETERS as GET_PAGE_LINKS_TOOL_PARAMETERS } from './tools/getPageLinks.js';
+import { createTool as createMovePageTool, PARAMETERS as MOVE_PAGE_TOOL_PARAMETERS } from './tools/movePage.js';
+import { createTool as createDeletePageTool, PARAMETERS as DELETE_PAGE_TOOL_PARAMETERS } from './tools/deletePage.js';
+import { createTool as createRestorePageVersionTool, PARAMETERS as RESTORE_PAGE_VERSION_TOOL_PARAMETERS } from './tools/restorePageVersion.js';
+import { createTool as createConvertPageEditorTool, PARAMETERS as CONVERT_PAGE_EDITOR_TOOL_PARAMETERS } from './tools/convertPageEditor.js';
+import { WikiJSClient } from '../wikijs/index.js';
 import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
-import { CONFIG } from '../config/index.js';
 
 export class WikiJSMcpServer {
     private server: McpServer;
     private wikiClient: WikiJSClient;
+    private enableEdit: boolean;
 
     constructor(wikiClient: WikiJSClient, options: {
         version: string;
         description: string;
+        enableEdit?: boolean;
     }) {
         this.server = new McpServer({
             name: 'wikijs-mcp-server',
@@ -24,13 +35,14 @@ export class WikiJSMcpServer {
         });
 
         this.wikiClient = wikiClient;
+        this.enableEdit = options.enableEdit ?? false;
         this._registerTools();
     }
 
     private _registerTools() {
         this.server.tool(
             'search_pages',
-            'Search for pages in WikiJS by query string',
+            'Search for pages in WikiJS by query string. Returns id, title, description, path, locale and url for each hit.',
             SEARCH_PAGES_TOOL_PARAMETERS,
             createSearchPagesTool(this.wikiClient));
 
@@ -50,12 +62,19 @@ export class WikiJSMcpServer {
         
         this.server.tool(
             'get_all_pages',
-            'Get all pages in WikiJS',
+            'List WikiJS pages (optionally filtered by locale/tags, capped by limit). Returns id, path, locale, title, description, dates, tags, isPublished, isPrivate and url.',
             GET_ALL_PAGES_TOOL_PARAMETERS,
             createGetAllPagesTool(this.wikiClient)
         );
 
-        if (CONFIG.ENABLE_EDIT) {
+        this.server.tool('get_page_history', 'Get revision history of a WikiJS page', GET_PAGE_HISTORY_TOOL_PARAMETERS, createGetPageHistoryTool(this.wikiClient));
+        this.server.tool('get_page_version', 'Get a specific version of a WikiJS page', GET_PAGE_VERSION_TOOL_PARAMETERS, createGetPageVersionTool(this.wikiClient));
+        this.server.tool('get_page_tree', 'Get the page tree of a WikiJS site', GET_PAGE_TREE_TOOL_PARAMETERS, createGetPageTreeTool(this.wikiClient));
+        this.server.tool('list_tags', 'List all tags in WikiJS', LIST_TAGS_TOOL_PARAMETERS, createListTagsTool(this.wikiClient));
+        this.server.tool('search_tags', 'Search tags in WikiJS', SEARCH_TAGS_TOOL_PARAMETERS, createSearchTagsTool(this.wikiClient));
+        this.server.tool('get_page_links', 'Get page link graph for a locale', GET_PAGE_LINKS_TOOL_PARAMETERS, createGetPageLinksTool(this.wikiClient));
+
+        if (this.enableEdit) {
             this.server.tool(
                 'create_page',
                 'Create a new WikiJS page',
@@ -69,6 +88,11 @@ export class WikiJSMcpServer {
                 EDIT_PAGE_TOOL_PARAMETERS,
                 createEditPageTool(this.wikiClient)
             );
+
+            this.server.tool('move_page', 'Move a WikiJS page to a new path/locale', MOVE_PAGE_TOOL_PARAMETERS, createMovePageTool(this.wikiClient));
+            this.server.tool('delete_page', 'Delete a WikiJS page', DELETE_PAGE_TOOL_PARAMETERS, createDeletePageTool(this.wikiClient));
+            this.server.tool('restore_page_version', 'Restore a previous version of a WikiJS page', RESTORE_PAGE_VERSION_TOOL_PARAMETERS, createRestorePageVersionTool(this.wikiClient));
+            this.server.tool('convert_page_editor', 'Convert a WikiJS page to another editor type', CONVERT_PAGE_EDITOR_TOOL_PARAMETERS, createConvertPageEditorTool(this.wikiClient));
         }
     }
     

+ 24 - 0
src/mcp/tools/convertPageEditor.ts

@@ -0,0 +1,24 @@
+import { z } from 'zod';
+import { ToolCallback } from '@modelcontextprotocol/sdk/server/mcp.js';
+import { WikiJSClient } from '../../wikijs/index.js';
+
+export const PARAMETERS = {
+  id: z.coerce.number().int().positive().describe('Page ID'),
+  editor: z.string().min(1).describe('Target editor type (e.g. markdown, visual)')
+};
+
+export const createTool = (wikiClient: WikiJSClient): ToolCallback<typeof PARAMETERS> => {
+  return async (request) => {
+    try {
+      const { id, editor } = request;
+      const result = await wikiClient.convertPageEditor(id, editor);
+      if (!result?.responseResult?.succeeded) {
+        return { content: [{ type: 'text', text: `Failed to convert editor: ${result?.responseResult?.message || 'Unknown error'}` }], isError: true };
+      }
+      return { content: [{ type: 'text', text: JSON.stringify({ succeeded: true, message: result.responseResult.message }, null, 2) }] };
+    } catch (error) {
+      const errorMessage = error instanceof Error ? error.message : String(error);
+      return { content: [{ type: 'text', text: `Error converting editor: ${errorMessage}` }], isError: true };
+    }
+  };
+};

+ 39 - 37
src/mcp/tools/createPage.ts

@@ -1,59 +1,61 @@
-import { WikiJSClient } from '../../wikijs/index.js';
 import { z } from 'zod';
 import { ToolCallback } from '@modelcontextprotocol/sdk/server/mcp.js';
+import { WikiJSClient } from '../../wikijs/index.js';
 
 export const PARAMETERS = {
-  title: z.string().describe('The title of the page'),
-  content: z.string().describe('The content of the page'),
+  title: z.string().min(1).describe('The title of the page'),
+  content: z.string().min(1).describe('The content of the page'),
   description: z.string().describe('The description of the page'),
-  path: z.string().describe('The path of the page'),
+  path: z.string().min(1).describe('The path of the page'),
   editor: z.string().default('markdown').describe('The editor type (e.g., markdown, visual)'),
   isPublished: z.boolean().default(true).describe('Whether the page is published'),
   isPrivate: z.boolean().default(false).describe('Whether the page is private'),
-  locale: z.string().default('en').describe('The locale of the page'),
+  locale: z.string().min(2).max(10).default('en').describe('The locale of the page'),
   tags: z.array(z.string()).default([]).describe('Tags for the page')
 };
 
 export const createTool = (wikiClient: WikiJSClient): ToolCallback<typeof PARAMETERS> => {
   return async (params) => {
-    const result = await wikiClient.createPage({
-      title: params.title,
-      content: params.content,
-      description: params.description,
-      path: params.path,
-      editor: params.editor,
-      isPublished: params.isPublished,
-      isPrivate: params.isPrivate,
-      locale: params.locale,
-      tags: params.tags
-    });
+    try {
+      const result = await wikiClient.createPage({
+        title: params.title,
+        content: params.content,
+        description: params.description,
+        path: params.path,
+        editor: params.editor,
+        isPublished: params.isPublished,
+        isPrivate: params.isPrivate,
+        locale: params.locale,
+        tags: params.tags
+      });
 
-    if (!result) {
-      return {
-        content: [{
-          type: "text",
-          text: "Failed to create page"
-        }]
-      };
-    }
+      if (!result) {
+        return { content: [{ type: 'text', text: 'Failed to create page' }], isError: true };
+      }
+
+      if (!result.responseResult?.succeeded) {
+        return {
+          content: [{
+            type: 'text',
+            text: `Failed to create page: ${result.responseResult?.message || 'Unknown error'} (Error code: ${result.responseResult?.errorCode})`
+          }],
+          isError: true
+        };
+      }
 
-    if (!result.responseResult?.succeeded) {
       return {
         content: [{
-          type: "text",
-          text: `Failed to create page: ${result.responseResult?.message || 'Unknown error'} (Error code: ${result.responseResult?.errorCode})`
+          type: 'text',
+          text: JSON.stringify({
+            succeeded: true,
+            message: result.responseResult?.message || 'Page created successfully',
+            page: result.page
+          }, null, 2)
         }]
       };
+    } catch (error) {
+      const errorMessage = error instanceof Error ? error.message : String(error);
+      return { content: [{ type: 'text', text: `Error creating page: ${errorMessage}` }], isError: true };
     }
-
-    return {
-      content: [{
-        type: "text",
-        text: JSON.stringify({
-          succeeded: true,
-          message: result.responseResult?.message || 'Page created successfully'
-        }, null, 2)
-      }]
-    };
   };
 };

+ 23 - 0
src/mcp/tools/deletePage.ts

@@ -0,0 +1,23 @@
+import { z } from 'zod';
+import { ToolCallback } from '@modelcontextprotocol/sdk/server/mcp.js';
+import { WikiJSClient } from '../../wikijs/index.js';
+
+export const PARAMETERS = {
+  id: z.coerce.number().int().positive().describe('Page ID to delete')
+};
+
+export const createTool = (wikiClient: WikiJSClient): ToolCallback<typeof PARAMETERS> => {
+  return async (request) => {
+    try {
+      const { id } = request;
+      const result = await wikiClient.deletePage(id);
+      if (!result?.responseResult?.succeeded) {
+        return { content: [{ type: 'text', text: `Failed to delete page: ${result?.responseResult?.message || 'Unknown error'}` }], isError: true };
+      }
+      return { content: [{ type: 'text', text: JSON.stringify({ succeeded: true, message: result.responseResult.message }, null, 2) }] };
+    } catch (error) {
+      const errorMessage = error instanceof Error ? error.message : String(error);
+      return { content: [{ type: 'text', text: `Error deleting page: ${errorMessage}` }], isError: true };
+    }
+  };
+};

+ 46 - 33
src/mcp/tools/editPage.ts

@@ -3,48 +3,61 @@ import { ToolCallback } from '@modelcontextprotocol/sdk/server/mcp.js';
 import { WikiJSClient } from '../../wikijs/index.js';
 
 export const PARAMETERS = {
-  id: z.number().describe('The ID of the page to update'),
-  title: z.string().describe('The new title of the page'),
-  content: z.string().describe('The new content of the page'),
-  description: z.string().describe('The new description of the page'),
-  editor: z.string().default('markdown').describe('The editor type (e.g., markdown, visual)'),
-  isPublished: z.boolean().default(true).describe('Whether the page is published'),
-  isPrivate: z.boolean().default(false).describe('Whether the page is private'),
-  locale: z.string().default('en').describe('The locale of the page'),
-  tags: z.array(z.string()).default([]).describe('Tags for the page'),
-  path: z.string().describe('The path of the page')
+  id: z.coerce.number().int().positive().describe('The ID of the page to update'),
+  content: z.string().min(1).describe('The new FULL content of the page (replaces existing content)'),
+  title: z.string().min(1).optional().describe('New title (omit to keep existing)'),
+  description: z.string().optional().describe('New description (omit to keep existing)'),
+  path: z.string().min(1).optional().describe('New path (omit to keep existing)'),
+  editor: z.string().optional().describe('Editor type (e.g., markdown, visual) (omit to keep existing)'),
+  isPublished: z.boolean().optional().describe('Whether the page is published (omit to keep existing)'),
+  isPrivate: z.boolean().optional().describe('Whether the page is private (omit to keep existing)'),
+  locale: z.string().min(2).max(10).optional().describe('Locale of the page (omit to keep existing)'),
+  tags: z.array(z.string()).optional().describe('Tags for the page (omit to keep existing)')
 };
 
 export const createTool = (wikiClient: WikiJSClient): ToolCallback<typeof PARAMETERS> => {
   return async (params) => {
-    const result = await wikiClient.updatePage(params);
-    
-    if (!result) {
-      return {
-        content: [{
-          type: "text",
-          text: "Failed to update page"
-        }]
-      };
-    }
+    try {
+      const result = await wikiClient.updatePage({
+        id: params.id,
+        content: params.content,
+        title: params.title,
+        description: params.description,
+        path: params.path,
+        editor: params.editor,
+        isPublished: params.isPublished,
+        isPrivate: params.isPrivate,
+        locale: params.locale,
+        tags: params.tags
+      });
+
+      if (!result) {
+        return { content: [{ type: 'text', text: 'Failed to update page' }], isError: true };
+      }
+
+      if (!result.responseResult?.succeeded) {
+        return {
+          content: [{
+            type: 'text',
+            text: `Failed to update page: ${result.responseResult?.message || 'Unknown error'} (Error code: ${result.responseResult?.errorCode})`
+          }],
+          isError: true
+        };
+      }
 
-    if (!result.responseResult?.succeeded) {
       return {
         content: [{
-          type: "text",
-          text: `Failed to update page: ${result.responseResult?.message || 'Unknown error'} (Error code: ${result.responseResult?.errorCode})`
+          type: 'text',
+          text: JSON.stringify({
+            succeeded: true,
+            message: result.responseResult?.message || 'Page updated successfully',
+            page: result.page
+          }, null, 2)
         }]
       };
+    } catch (error) {
+      const errorMessage = error instanceof Error ? error.message : String(error);
+      return { content: [{ type: 'text', text: `Error updating page: ${errorMessage}` }], isError: true };
     }
-
-    return {
-      content: [{
-        type: "text",
-        text: JSON.stringify({
-          succeeded: true,
-          message: result.responseResult?.message || 'Page updated successfully'
-        }, null, 2)
-      }]
-    };
   };
 };

+ 23 - 28
src/mcp/tools/getAllPages.ts

@@ -1,33 +1,28 @@
+import { z } from 'zod';
 import { ToolCallback } from '@modelcontextprotocol/sdk/server/mcp.js';
-import { WikiJSClient } from '../../wikijs';
-
-export const PARAMETERS = {};
+import { WikiJSClient } from '../../wikijs/index.js';
 
+export const PARAMETERS = {
+  limit: z.number().int().positive().max(500).optional().describe('Maximum number of pages to return (default: all, capped at 500)'),
+  locale: z.string().min(2).max(10).optional().describe('Optional locale filter (e.g. "en")'),
+  tags: z.array(z.string()).optional().describe('Optional tag filter')
+};
 
 export const createTool = (wikiClient: WikiJSClient): ToolCallback<typeof PARAMETERS> => {
-	return async () => {
-		try {
-			const result = await wikiClient.getAllPages();
+  return async (request) => {
+    try {
+      const { limit, locale, tags } = request;
+      const result = await wikiClient.getAllPages({ limit, locale, tags });
 
-			return {
-				content: [
-					{
-						type: 'text',
-						text: JSON.stringify(result, null, 2)
-					}
-				]
-			};
-		} catch (error) {
-			const errorMessage = error instanceof Error ? error.message : String(error);
-			return {
-				content: [
-					{
-						type: 'text',
-						text: `Error getting page by ID: ${errorMessage}`
-					}
-				],
-				isError: true
-			};
-		}
-	}
-}
+      return {
+        content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
+      };
+    } catch (error) {
+      const errorMessage = error instanceof Error ? error.message : String(error);
+      return {
+        content: [{ type: 'text', text: `Error getting all pages: ${errorMessage}` }],
+        isError: true
+      };
+    }
+  };
+};

+ 26 - 31
src/mcp/tools/getPageById.ts

@@ -1,38 +1,33 @@
 import { z } from 'zod';
 import { ToolCallback } from '@modelcontextprotocol/sdk/server/mcp.js';
-import { WikiJSClient } from '../../wikijs';
+import { WikiJSClient } from '../../wikijs/index.js';
 
-
-export const PARAMETERS ={
-    id: z.number().describe('The ID of the page to retrieve')
+export const PARAMETERS = {
+  id: z.coerce.number().int().positive().describe('The ID of the page to retrieve')
 };
 
-
 export const createTool = (wikiClient: WikiJSClient): ToolCallback<typeof PARAMETERS> => {
-	return async (request) => {
-		try {
-			const { id } = request;
-			const result = await wikiClient.getPageById(id);
+  return async (request) => {
+    try {
+      const { id } = request;
+      const result = await wikiClient.getPageById(id);
+
+      if (!result) {
+        return {
+          content: [{ type: 'text', text: `No page found for id ${id}` }],
+          isError: true
+        };
+      }
 
-			return {
-				content: [
-					{
-						type: 'text',
-						text: JSON.stringify(result, null, 2)
-					}
-				]
-			};
-		} catch (error) {
-			const errorMessage = error instanceof Error ? error.message : String(error);
-			return {
-				content: [
-					{
-						type: 'text',
-						text: `Error getting page by ID: ${errorMessage}`
-					}
-				],
-				isError: true
-			};
-		}
-	}
-}
+      return {
+        content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
+      };
+    } catch (error) {
+      const errorMessage = error instanceof Error ? error.message : String(error);
+      return {
+        content: [{ type: 'text', text: `Error getting page by ID: ${errorMessage}` }],
+        isError: true
+      };
+    }
+  };
+};

+ 27 - 30
src/mcp/tools/getPageByPath.ts

@@ -1,39 +1,36 @@
 import { z } from 'zod';
 import { ToolCallback } from '@modelcontextprotocol/sdk/server/mcp.js';
-import { WikiJSClient } from '../../wikijs';
+import { WikiJSClient } from '../../wikijs/index.js';
 
+export const LOCALE_SCHEMA = z.string().min(2).max(10).describe('Locale code of the page (e.g. "en", "de", "zh-Hans")');
 
 export const PARAMETERS = {
-	path: z.string().describe('The path of the page to retrieve'),
-	locale: z.string().regex(/^[a-z]{2}(-[A-Z]{2})?$/, 'Invalid locale format').describe('The locale of the page (e.g., "en")')
-}
+  path: z.string().min(1).describe('The path of the page to retrieve'),
+  locale: LOCALE_SCHEMA
+};
 
 export const createTool = (wikiClient: WikiJSClient): ToolCallback<typeof PARAMETERS> => {
-	return 	async (request) => {
-		try {
-			const { path, locale } = request;
+  return async (request) => {
+    try {
+      const { path, locale } = request;
+      const result = await wikiClient.getPageByPath(path, locale);
 
-			const result = await wikiClient.getPageByPath(path, locale);
+      if (!result) {
+        return {
+          content: [{ type: 'text', text: `No page found for path "${path}" (locale ${locale})` }],
+          isError: true
+        };
+      }
 
-			return {
-				content: [
-					{
-						type: 'text',
-						text: JSON.stringify(result, null, 2)
-					}
-				]
-			};
-		} catch (error) {
-			const errorMessage = error instanceof Error ? error.message : String(error);
-			return {
-				content: [
-					{
-						type: 'text',
-						text: `Error getting page by path: ${errorMessage}`
-					}
-				],
-				isError: true
-			};
-		}
-	}
-}
+      return {
+        content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
+      };
+    } catch (error) {
+      const errorMessage = error instanceof Error ? error.message : String(error);
+      return {
+        content: [{ type: 'text', text: `Error getting page by path: ${errorMessage}` }],
+        isError: true
+      };
+    }
+  };
+};

+ 25 - 0
src/mcp/tools/getPageHistory.ts

@@ -0,0 +1,25 @@
+import { z } from 'zod';
+import { ToolCallback } from '@modelcontextprotocol/sdk/server/mcp.js';
+import { WikiJSClient } from '../../wikijs/index.js';
+
+export const PARAMETERS = {
+  id: z.coerce.number().int().positive().describe('Page ID'),
+  offsetPage: z.number().int().min(0).optional().describe('Page offset for history (default 0)'),
+  offsetSize: z.number().int().positive().max(200).optional().describe('Page size (default 100)')
+};
+
+export const createTool = (wikiClient: WikiJSClient): ToolCallback<typeof PARAMETERS> => {
+  return async (request) => {
+    try {
+      const { id, offsetPage, offsetSize } = request;
+      const result = await wikiClient.getPageHistory(id, offsetPage, offsetSize);
+      if (!result) {
+        return { content: [{ type: 'text', text: `No history found for page ${id}` }], isError: true };
+      }
+      return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
+    } catch (error) {
+      const errorMessage = error instanceof Error ? error.message : String(error);
+      return { content: [{ type: 'text', text: `Error getting page history: ${errorMessage}` }], isError: true };
+    }
+  };
+};

+ 20 - 0
src/mcp/tools/getPageLinks.ts

@@ -0,0 +1,20 @@
+import { z } from 'zod';
+import { ToolCallback } from '@modelcontextprotocol/sdk/server/mcp.js';
+import { WikiJSClient } from '../../wikijs/index.js';
+
+export const PARAMETERS = {
+  locale: z.string().min(2).max(10).describe('Locale (e.g. "en")')
+};
+
+export const createTool = (wikiClient: WikiJSClient): ToolCallback<typeof PARAMETERS> => {
+  return async (request) => {
+    try {
+      const { locale } = request;
+      const result = await wikiClient.getPageLinks(locale);
+      return { content: [{ type: 'text', text: JSON.stringify(result ?? [], null, 2) }] };
+    } catch (error) {
+      const errorMessage = error instanceof Error ? error.message : String(error);
+      return { content: [{ type: 'text', text: `Error getting page links: ${errorMessage}` }], isError: true };
+    }
+  };
+};

+ 24 - 0
src/mcp/tools/getPageTree.ts

@@ -0,0 +1,24 @@
+import { z } from 'zod';
+import { ToolCallback } from '@modelcontextprotocol/sdk/server/mcp.js';
+import { WikiJSClient } from '../../wikijs/index.js';
+
+export const PARAMETERS = {
+  locale: z.string().min(2).max(10).describe('Locale (e.g. "en")'),
+  mode: z.enum(['FOLDERS', 'PAGES', 'ALL']).default('PAGES').describe('Tree mode'),
+  path: z.string().optional().describe('Optional path prefix'),
+  parent: z.coerce.number().int().optional().describe('Optional parent page id'),
+  includeAncestors: z.boolean().optional().describe('Include ancestors')
+};
+
+export const createTool = (wikiClient: WikiJSClient): ToolCallback<typeof PARAMETERS> => {
+  return async (request) => {
+    try {
+      const { locale, mode, path, parent, includeAncestors } = request;
+      const result = await wikiClient.getPageTree({ path, parent, mode, locale, includeAncestors });
+      return { content: [{ type: 'text', text: JSON.stringify(result ?? [], null, 2) }] };
+    } catch (error) {
+      const errorMessage = error instanceof Error ? error.message : String(error);
+      return { content: [{ type: 'text', text: `Error getting page tree: ${errorMessage}` }], isError: true };
+    }
+  };
+};

+ 24 - 0
src/mcp/tools/getPageVersion.ts

@@ -0,0 +1,24 @@
+import { z } from 'zod';
+import { ToolCallback } from '@modelcontextprotocol/sdk/server/mcp.js';
+import { WikiJSClient } from '../../wikijs/index.js';
+
+export const PARAMETERS = {
+  pageId: z.coerce.number().int().positive().describe('Page ID'),
+  versionId: z.coerce.number().int().positive().describe('Version ID')
+};
+
+export const createTool = (wikiClient: WikiJSClient): ToolCallback<typeof PARAMETERS> => {
+  return async (request) => {
+    try {
+      const { pageId, versionId } = request;
+      const result = await wikiClient.getPageVersion(pageId, versionId);
+      if (!result) {
+        return { content: [{ type: 'text', text: `No version found` }], isError: true };
+      }
+      return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
+    } catch (error) {
+      const errorMessage = error instanceof Error ? error.message : String(error);
+      return { content: [{ type: 'text', text: `Error getting page version: ${errorMessage}` }], isError: true };
+    }
+  };
+};

+ 16 - 0
src/mcp/tools/listTags.ts

@@ -0,0 +1,16 @@
+import { ToolCallback } from '@modelcontextprotocol/sdk/server/mcp.js';
+import { WikiJSClient } from '../../wikijs/index.js';
+
+export const PARAMETERS = {};
+
+export const createTool = (wikiClient: WikiJSClient): ToolCallback<typeof PARAMETERS> => {
+  return async () => {
+    try {
+      const result = await wikiClient.listTags();
+      return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
+    } catch (error) {
+      const errorMessage = error instanceof Error ? error.message : String(error);
+      return { content: [{ type: 'text', text: `Error listing tags: ${errorMessage}` }], isError: true };
+    }
+  };
+};

+ 25 - 0
src/mcp/tools/movePage.ts

@@ -0,0 +1,25 @@
+import { z } from 'zod';
+import { ToolCallback } from '@modelcontextprotocol/sdk/server/mcp.js';
+import { WikiJSClient } from '../../wikijs/index.js';
+
+export const PARAMETERS = {
+  id: z.coerce.number().int().positive().describe('Page ID to move'),
+  destinationPath: z.string().min(1).describe('Destination path'),
+  destinationLocale: z.string().min(2).max(10).describe('Destination locale')
+};
+
+export const createTool = (wikiClient: WikiJSClient): ToolCallback<typeof PARAMETERS> => {
+  return async (request) => {
+    try {
+      const { id, destinationPath, destinationLocale } = request;
+      const result = await wikiClient.movePage(id, destinationPath, destinationLocale);
+      if (!result?.responseResult?.succeeded) {
+        return { content: [{ type: 'text', text: `Failed to move page: ${result?.responseResult?.message || 'Unknown error'}` }], isError: true };
+      }
+      return { content: [{ type: 'text', text: JSON.stringify({ succeeded: true, message: result.responseResult.message }, null, 2) }] };
+    } catch (error) {
+      const errorMessage = error instanceof Error ? error.message : String(error);
+      return { content: [{ type: 'text', text: `Error moving page: ${errorMessage}` }], isError: true };
+    }
+  };
+};

+ 24 - 0
src/mcp/tools/restorePageVersion.ts

@@ -0,0 +1,24 @@
+import { z } from 'zod';
+import { ToolCallback } from '@modelcontextprotocol/sdk/server/mcp.js';
+import { WikiJSClient } from '../../wikijs/index.js';
+
+export const PARAMETERS = {
+  pageId: z.coerce.number().int().positive().describe('Page ID'),
+  versionId: z.coerce.number().int().positive().describe('Version ID to restore')
+};
+
+export const createTool = (wikiClient: WikiJSClient): ToolCallback<typeof PARAMETERS> => {
+  return async (request) => {
+    try {
+      const { pageId, versionId } = request;
+      const result = await wikiClient.restorePageVersion(pageId, versionId);
+      if (!result?.responseResult?.succeeded) {
+        return { content: [{ type: 'text', text: `Failed to restore version: ${result?.responseResult?.message || 'Unknown error'}` }], isError: true };
+      }
+      return { content: [{ type: 'text', text: JSON.stringify({ succeeded: true, message: result.responseResult.message }, null, 2) }] };
+    } catch (error) {
+      const errorMessage = error instanceof Error ? error.message : String(error);
+      return { content: [{ type: 'text', text: `Error restoring version: ${errorMessage}` }], isError: true };
+    }
+  };
+};

+ 22 - 20
src/mcp/tools/searchPage.ts

@@ -1,28 +1,30 @@
 import { z } from 'zod';
 import { ToolCallback } from '@modelcontextprotocol/sdk/server/mcp.js';
-import { WikiJSClient } from '../../wikijs';
+import { WikiJSClient } from '../../wikijs/index.js';
 
+export const LOCALE_SCHEMA = z.string().min(2).max(10).optional().describe('Optional locale for the search (e.g. "en")');
 
 export const PARAMETERS = {
-	query: z.string().min(1, 'Query cannot be empty').describe('Search query to find pages'),
-	path: z.string().optional().describe('Optional path to limit search scope'),
-	locale: z.string().regex(/^[a-z]{2}(-[A-Z]{2})?$/, 'Invalid locale format').optional().describe('Optional locale for the search (e.g., "en")')
+  query: z.string().min(1, 'Query cannot be empty').describe('Search query to find pages'),
+  path: z.string().optional().describe('Optional path prefix to limit search scope'),
+  locale: LOCALE_SCHEMA
 };
 
-
 export const createTool = (wikiClient: WikiJSClient): ToolCallback<typeof PARAMETERS> => {
-	return async (request) => {
-		const { query, path, locale } = request
-	
-		const result = await wikiClient.searchPages(query, path, locale);
-	
-		return {
-			content: [
-				{
-					type: 'text',
-					text: JSON.stringify(result, null, 2)
-				}
-			]
-		};
-	}
-}
+  return async (request) => {
+    try {
+      const { query, path, locale } = request;
+      const result = await wikiClient.searchPages(query, path, locale);
+
+      return {
+        content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
+      };
+    } catch (error) {
+      const errorMessage = error instanceof Error ? error.message : String(error);
+      return {
+        content: [{ type: 'text', text: `Error searching pages: ${errorMessage}` }],
+        isError: true
+      };
+    }
+  };
+};

+ 20 - 0
src/mcp/tools/searchTags.ts

@@ -0,0 +1,20 @@
+import { z } from 'zod';
+import { ToolCallback } from '@modelcontextprotocol/sdk/server/mcp.js';
+import { WikiJSClient } from '../../wikijs/index.js';
+
+export const PARAMETERS = {
+  query: z.string().min(1).describe('Tag search query')
+};
+
+export const createTool = (wikiClient: WikiJSClient): ToolCallback<typeof PARAMETERS> => {
+  return async (request) => {
+    try {
+      const { query } = request;
+      const result = await wikiClient.searchTags(query);
+      return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
+    } catch (error) {
+      const errorMessage = error instanceof Error ? error.message : String(error);
+      return { content: [{ type: 'text', text: `Error searching tags: ${errorMessage}` }], isError: true };
+    }
+  };
+};

+ 4 - 0
src/util/version.ts

@@ -4,6 +4,10 @@ import { dirname, join } from 'path';
 
 let cachedVersion: string | null = null;
 
+export function __resetVersionCache(): void {
+  cachedVersion = null;
+}
+
 export function getVersion(): string {
   if (cachedVersion !== null) {
     return cachedVersion;

+ 360 - 28
src/wikijs/generated/graphql.ts

@@ -16,7 +16,6 @@ export type Scalars = {
   Int: { input: number; output: number; }
   Float: { input: number; output: number; }
   Date: { input: any; output: any; }
-  Upload: { input: any; output: any; }
 };
 
 /** Mutations for Analytics */
@@ -307,11 +306,6 @@ export type AuthenticationStrategyInput = {
   strategyKey: Scalars['String']['input'];
 };
 
-export enum CacheControlScope {
-  Private = 'PRIVATE',
-  Public = 'PUBLIC'
-}
-
 export type CommentCreateResponse = {
   __typename?: 'CommentCreateResponse';
   id?: Maybe<Scalars['Int']['output']>;
@@ -422,7 +416,6 @@ export type ContributeQuery = {
   contributors?: Maybe<Array<Maybe<ContributeContributor>>>;
 };
 
-/** Generic Mutation Response */
 export type DefaultResponse = {
   __typename?: 'DefaultResponse';
   responseResult?: Maybe<ResponseStatus>;
@@ -514,14 +507,12 @@ export type GroupResponse = {
   responseResult: ResponseStatus;
 };
 
-/** Generic Key Value Pair */
 export type KeyValuePair = {
   __typename?: 'KeyValuePair';
   key: Scalars['String']['output'];
   value: Scalars['String']['output'];
 };
 
-/** General Key Value Pair Input */
 export type KeyValuePairInput = {
   key: Scalars['String']['input'];
   value: Scalars['String']['input'];
@@ -677,7 +668,6 @@ export type MailQuery = {
   config?: Maybe<MailConfig>;
 };
 
-/** Mutations (Create, Update, Delete) */
 export type Mutation = {
   __typename?: 'Mutation';
   analytics?: Maybe<AnalyticsMutation>;
@@ -1167,7 +1157,6 @@ export type PageVersion = {
   versionId: Scalars['Int']['output'];
 };
 
-/** Query (Read) */
 export type Query = {
   __typename?: 'Query';
   analytics?: Maybe<AnalyticsQuery>;
@@ -1230,7 +1219,6 @@ export type RenderingQueryRenderersArgs = {
   orderBy?: InputMaybe<Scalars['String']['input']>;
 };
 
-/** Mutation Status */
 export type ResponseStatus = {
   __typename?: 'ResponseStatus';
   errorCode: Scalars['Int']['output'];
@@ -1444,7 +1432,6 @@ export type StorageTargetInput = {
   syncInterval?: InputMaybe<Scalars['String']['input']>;
 };
 
-/** Subscriptions (Push, Real-time) */
 export type Subscription = {
   __typename?: 'Subscription';
   loggingLiveTrail?: Maybe<LoggerTrailLine>;
@@ -1823,7 +1810,7 @@ export type GetPageByIdQueryVariables = Exact<{
 }>;
 
 
-export type GetPageByIdQuery = { __typename?: 'Query', pages?: { __typename?: 'PageQuery', single?: { __typename?: 'Page', id: number, path: string, title: string, description: string, content: string, createdAt: any, updatedAt: any, locale: string, tags: Array<{ __typename?: 'PageTag', tag: string, title?: string | null } | null> } | null } | null };
+export type GetPageByIdQuery = { __typename?: 'Query', pages?: { __typename?: 'PageQuery', single?: { __typename?: 'Page', id: number, path: string, title: string, description: string, content: string, createdAt: any, updatedAt: any, locale: string, editor: string, isPublished: boolean, isPrivate: boolean, render?: string | null, toc?: string | null, contentType: string, publishStartDate: any, publishEndDate: any, scriptCss?: string | null, scriptJs?: string | null, tags: Array<{ __typename?: 'PageTag', tag: string, title?: string | null } | null> } | null } | null };
 
 export type GetPageByPathQueryVariables = Exact<{
   path: Scalars['String']['input'];
@@ -1831,7 +1818,7 @@ export type GetPageByPathQueryVariables = Exact<{
 }>;
 
 
-export type GetPageByPathQuery = { __typename?: 'Query', pages?: { __typename?: 'PageQuery', singleByPath?: { __typename?: 'Page', id: number, path: string, title: string, description: string, content: string, locale: string, tags: Array<{ __typename?: 'PageTag', tag: string, title?: string | null } | null> } | null } | null };
+export type GetPageByPathQuery = { __typename?: 'Query', pages?: { __typename?: 'PageQuery', singleByPath?: { __typename?: 'Page', id: number, path: string, title: string, description: string, content: string, locale: string, editor: string, isPublished: boolean, isPrivate: boolean, render?: string | null, toc?: string | null, contentType: string, publishStartDate: any, publishEndDate: any, scriptCss?: string | null, scriptJs?: string | null, tags: Array<{ __typename?: 'PageTag', tag: string, title?: string | null } | null> } | null } | null };
 
 export type GetAllPagesQueryVariables = Exact<{
   limit?: InputMaybe<Scalars['Int']['input']>;
@@ -1840,7 +1827,7 @@ export type GetAllPagesQueryVariables = Exact<{
 }>;
 
 
-export type GetAllPagesQuery = { __typename?: 'Query', pages?: { __typename?: 'PageQuery', list: Array<{ __typename?: 'PageListItem', id: number, path: string, locale: string, title?: string | null, description?: string | null, createdAt: any, updatedAt: any, tags?: Array<string | null> | null }> } | null };
+export type GetAllPagesQuery = { __typename?: 'Query', pages?: { __typename?: 'PageQuery', list: Array<{ __typename?: 'PageListItem', id: number, path: string, locale: string, title?: string | null, description?: string | null, createdAt: any, updatedAt: any, tags?: Array<string | null> | null, isPublished: boolean, isPrivate: boolean }> } | null };
 
 export type SearchPagesQueryVariables = Exact<{
   query: Scalars['String']['input'];
@@ -1849,7 +1836,7 @@ export type SearchPagesQueryVariables = Exact<{
 }>;
 
 
-export type SearchPagesQuery = { __typename?: 'Query', pages?: { __typename?: 'PageQuery', search: { __typename?: 'PageSearchResponse', suggestions: Array<string | null>, totalHits: number, results: Array<{ __typename?: 'PageSearchResult', title: string, description: string, path: string, locale: string } | null> } } | null };
+export type SearchPagesQuery = { __typename?: 'Query', pages?: { __typename?: 'PageQuery', search: { __typename?: 'PageSearchResponse', suggestions: Array<string | null>, totalHits: number, results: Array<{ __typename?: 'PageSearchResult', id: string, title: string, description: string, path: string, locale: string } | null> } } | null };
 
 export type CreatePageMutationVariables = Exact<{
   title: Scalars['String']['input'];
@@ -1864,23 +1851,117 @@ export type CreatePageMutationVariables = Exact<{
 }>;
 
 
-export type CreatePageMutation = { __typename?: 'Mutation', pages?: { __typename?: 'PageMutation', create?: { __typename?: 'PageResponse', responseResult: { __typename?: 'ResponseStatus', succeeded: boolean, errorCode: number, slug: string, message?: string | null } } | null } | null };
+export type CreatePageMutation = { __typename?: 'Mutation', pages?: { __typename?: 'PageMutation', create?: { __typename?: 'PageResponse', responseResult: { __typename?: 'ResponseStatus', succeeded: boolean, errorCode: number, slug: string, message?: string | null }, page?: { __typename?: 'Page', id: number, path: string, locale: string } | null } | null } | null };
 
 export type UpdatePageMutationVariables = Exact<{
   id: Scalars['Int']['input'];
-  title: Scalars['String']['input'];
-  content: Scalars['String']['input'];
-  description: Scalars['String']['input'];
-  editor: Scalars['String']['input'];
-  isPublished: Scalars['Boolean']['input'];
-  isPrivate: Scalars['Boolean']['input'];
+  title?: InputMaybe<Scalars['String']['input']>;
+  content?: InputMaybe<Scalars['String']['input']>;
+  description?: InputMaybe<Scalars['String']['input']>;
+  editor?: InputMaybe<Scalars['String']['input']>;
+  isPublished?: InputMaybe<Scalars['Boolean']['input']>;
+  isPrivate?: InputMaybe<Scalars['Boolean']['input']>;
+  locale?: InputMaybe<Scalars['String']['input']>;
+  tags?: InputMaybe<Array<InputMaybe<Scalars['String']['input']>> | InputMaybe<Scalars['String']['input']>>;
+  path?: InputMaybe<Scalars['String']['input']>;
+}>;
+
+
+export type UpdatePageMutation = { __typename?: 'Mutation', pages?: { __typename?: 'PageMutation', update?: { __typename?: 'PageResponse', responseResult: { __typename?: 'ResponseStatus', succeeded: boolean, errorCode: number, slug: string, message?: string | null }, page?: { __typename?: 'Page', id: number, path: string, locale: string } | null } | null } | null };
+
+export type GetPageHistoryQueryVariables = Exact<{
+  id: Scalars['Int']['input'];
+  offsetPage?: InputMaybe<Scalars['Int']['input']>;
+  offsetSize?: InputMaybe<Scalars['Int']['input']>;
+}>;
+
+
+export type GetPageHistoryQuery = { __typename?: 'Query', pages?: { __typename?: 'PageQuery', history?: { __typename?: 'PageHistoryResult', total: number, trail?: Array<{ __typename?: 'PageHistory', versionId: number, versionDate: any, authorId: number, authorName: string, actionType: string, valueBefore?: string | null, valueAfter?: string | null } | null> | null } | null } | null };
+
+export type GetPageVersionQueryVariables = Exact<{
+  pageId: Scalars['Int']['input'];
+  versionId: Scalars['Int']['input'];
+}>;
+
+
+export type GetPageVersionQuery = { __typename?: 'Query', pages?: { __typename?: 'PageQuery', version?: { __typename?: 'PageVersion', action: string, authorId: string, authorName: string, content: string, contentType: string, createdAt: any, versionDate: any, description: string, editor: string, isPrivate: boolean, isPublished: boolean, locale: string, pageId: number, path: string, publishEndDate: any, publishStartDate: any, tags: Array<string | null>, title: string, versionId: number } | null } | null };
+
+export type GetPageTreeQueryVariables = Exact<{
+  path?: InputMaybe<Scalars['String']['input']>;
+  parent?: InputMaybe<Scalars['Int']['input']>;
+  mode: PageTreeMode;
   locale: Scalars['String']['input'];
-  tags: Array<InputMaybe<Scalars['String']['input']>> | InputMaybe<Scalars['String']['input']>;
-  path: Scalars['String']['input'];
+  includeAncestors?: InputMaybe<Scalars['Boolean']['input']>;
 }>;
 
 
-export type UpdatePageMutation = { __typename?: 'Mutation', pages?: { __typename?: 'PageMutation', update?: { __typename?: 'PageResponse', responseResult: { __typename?: 'ResponseStatus', succeeded: boolean, errorCode: number, slug: string, message?: string | null } } | null } | null };
+export type GetPageTreeQuery = { __typename?: 'Query', pages?: { __typename?: 'PageQuery', tree?: Array<{ __typename?: 'PageTreeItem', id: number, path: string, depth: number, title: string, isPrivate: boolean, isFolder: boolean, privateNS?: string | null, parent?: number | null, pageId?: number | null, locale: string } | null> | null } | null };
+
+export type ListTagsQueryVariables = Exact<{ [key: string]: never; }>;
+
+
+export type ListTagsQuery = { __typename?: 'Query', pages?: { __typename?: 'PageQuery', tags: Array<{ __typename?: 'PageTag', id: number, tag: string, title?: string | null, createdAt: any, updatedAt: any } | null> } | null };
+
+export type SearchTagsQueryVariables = Exact<{
+  query: Scalars['String']['input'];
+}>;
+
+
+export type SearchTagsQuery = { __typename?: 'Query', pages?: { __typename?: 'PageQuery', searchTags: Array<string | null> } | null };
+
+export type GetPageLinksQueryVariables = Exact<{
+  locale: Scalars['String']['input'];
+}>;
+
+
+export type GetPageLinksQuery = { __typename?: 'Query', pages?: { __typename?: 'PageQuery', links?: Array<{ __typename?: 'PageLinkItem', id: number, path: string, title: string, links: Array<string | null> } | null> | null } | null };
+
+export type CheckConflictsQueryVariables = Exact<{
+  id: Scalars['Int']['input'];
+  checkoutDate: Scalars['Date']['input'];
+}>;
+
+
+export type CheckConflictsQuery = { __typename?: 'Query', pages?: { __typename?: 'PageQuery', checkConflicts: boolean } | null };
+
+export type MovePageMutationVariables = Exact<{
+  id: Scalars['Int']['input'];
+  destinationPath: Scalars['String']['input'];
+  destinationLocale: Scalars['String']['input'];
+}>;
+
+
+export type MovePageMutation = { __typename?: 'Mutation', pages?: { __typename?: 'PageMutation', move?: { __typename?: 'DefaultResponse', responseResult?: { __typename?: 'ResponseStatus', succeeded: boolean, errorCode: number, slug: string, message?: string | null } | null } | null } | null };
+
+export type DeletePageMutationVariables = Exact<{
+  id: Scalars['Int']['input'];
+}>;
+
+
+export type DeletePageMutation = { __typename?: 'Mutation', pages?: { __typename?: 'PageMutation', delete?: { __typename?: 'DefaultResponse', responseResult?: { __typename?: 'ResponseStatus', succeeded: boolean, errorCode: number, slug: string, message?: string | null } | null } | null } | null };
+
+export type RestorePageVersionMutationVariables = Exact<{
+  pageId: Scalars['Int']['input'];
+  versionId: Scalars['Int']['input'];
+}>;
+
+
+export type RestorePageVersionMutation = { __typename?: 'Mutation', pages?: { __typename?: 'PageMutation', restore?: { __typename?: 'DefaultResponse', responseResult?: { __typename?: 'ResponseStatus', succeeded: boolean, errorCode: number, slug: string, message?: string | null } | null } | null } | null };
+
+export type ConvertPageEditorMutationVariables = Exact<{
+  id: Scalars['Int']['input'];
+  editor: Scalars['String']['input'];
+}>;
+
+
+export type ConvertPageEditorMutation = { __typename?: 'Mutation', pages?: { __typename?: 'PageMutation', convert?: { __typename?: 'DefaultResponse', responseResult?: { __typename?: 'ResponseStatus', succeeded: boolean, errorCode: number, slug: string, message?: string | null } | null } | null } | null };
+
+export type RenderPageMutationVariables = Exact<{
+  id: Scalars['Int']['input'];
+}>;
+
+
+export type RenderPageMutation = { __typename?: 'Mutation', pages?: { __typename?: 'PageMutation', render?: { __typename?: 'DefaultResponse', responseResult?: { __typename?: 'ResponseStatus', succeeded: boolean, errorCode: number, slug: string, message?: string | null } | null } | null } | null };
 
 
 export const GetPageByIdDocument = gql`
@@ -1899,6 +1980,16 @@ export const GetPageByIdDocument = gql`
       createdAt
       updatedAt
       locale
+      editor
+      isPublished
+      isPrivate
+      render
+      toc
+      contentType
+      publishStartDate
+      publishEndDate
+      scriptCss
+      scriptJs
     }
   }
 }
@@ -1917,6 +2008,16 @@ export const GetPageByPathDocument = gql`
         tag
         title
       }
+      editor
+      isPublished
+      isPrivate
+      render
+      toc
+      contentType
+      publishStartDate
+      publishEndDate
+      scriptCss
+      scriptJs
     }
   }
 }
@@ -1933,6 +2034,8 @@ export const GetAllPagesDocument = gql`
       createdAt
       updatedAt
       tags
+      isPublished
+      isPrivate
     }
   }
 }
@@ -1942,6 +2045,7 @@ export const SearchPagesDocument = gql`
   pages {
     search(query: $query, path: $path, locale: $locale) {
       results {
+        id
         title
         description
         path
@@ -1973,12 +2077,17 @@ export const CreatePageDocument = gql`
         slug
         message
       }
+      page {
+        id
+        path
+        locale
+      }
     }
   }
 }
     `;
 export const UpdatePageDocument = gql`
-    mutation UpdatePage($id: Int!, $title: String!, $content: String!, $description: String!, $editor: String!, $isPublished: Boolean!, $isPrivate: Boolean!, $locale: String!, $tags: [String]!, $path: String!) {
+    mutation UpdatePage($id: Int!, $title: String, $content: String, $description: String, $editor: String, $isPublished: Boolean, $isPrivate: Boolean, $locale: String, $tags: [String], $path: String) {
   pages {
     update(
       id: $id
@@ -1998,6 +2107,193 @@ export const UpdatePageDocument = gql`
         slug
         message
       }
+      page {
+        id
+        path
+        locale
+      }
+    }
+  }
+}
+    `;
+export const GetPageHistoryDocument = gql`
+    query GetPageHistory($id: Int!, $offsetPage: Int, $offsetSize: Int) {
+  pages {
+    history(id: $id, offsetPage: $offsetPage, offsetSize: $offsetSize) {
+      trail {
+        versionId
+        versionDate
+        authorId
+        authorName
+        actionType
+        valueBefore
+        valueAfter
+      }
+      total
+    }
+  }
+}
+    `;
+export const GetPageVersionDocument = gql`
+    query GetPageVersion($pageId: Int!, $versionId: Int!) {
+  pages {
+    version(pageId: $pageId, versionId: $versionId) {
+      action
+      authorId
+      authorName
+      content
+      contentType
+      createdAt
+      versionDate
+      description
+      editor
+      isPrivate
+      isPublished
+      locale
+      pageId
+      path
+      publishEndDate
+      publishStartDate
+      tags
+      title
+      versionId
+    }
+  }
+}
+    `;
+export const GetPageTreeDocument = gql`
+    query GetPageTree($path: String, $parent: Int, $mode: PageTreeMode!, $locale: String!, $includeAncestors: Boolean) {
+  pages {
+    tree(
+      path: $path
+      parent: $parent
+      mode: $mode
+      locale: $locale
+      includeAncestors: $includeAncestors
+    ) {
+      id
+      path
+      depth
+      title
+      isPrivate
+      isFolder
+      privateNS
+      parent
+      pageId
+      locale
+    }
+  }
+}
+    `;
+export const ListTagsDocument = gql`
+    query ListTags {
+  pages {
+    tags {
+      id
+      tag
+      title
+      createdAt
+      updatedAt
+    }
+  }
+}
+    `;
+export const SearchTagsDocument = gql`
+    query SearchTags($query: String!) {
+  pages {
+    searchTags(query: $query)
+  }
+}
+    `;
+export const GetPageLinksDocument = gql`
+    query GetPageLinks($locale: String!) {
+  pages {
+    links(locale: $locale) {
+      id
+      path
+      title
+      links
+    }
+  }
+}
+    `;
+export const CheckConflictsDocument = gql`
+    query CheckConflicts($id: Int!, $checkoutDate: Date!) {
+  pages {
+    checkConflicts(id: $id, checkoutDate: $checkoutDate)
+  }
+}
+    `;
+export const MovePageDocument = gql`
+    mutation MovePage($id: Int!, $destinationPath: String!, $destinationLocale: String!) {
+  pages {
+    move(
+      id: $id
+      destinationPath: $destinationPath
+      destinationLocale: $destinationLocale
+    ) {
+      responseResult {
+        succeeded
+        errorCode
+        slug
+        message
+      }
+    }
+  }
+}
+    `;
+export const DeletePageDocument = gql`
+    mutation DeletePage($id: Int!) {
+  pages {
+    delete(id: $id) {
+      responseResult {
+        succeeded
+        errorCode
+        slug
+        message
+      }
+    }
+  }
+}
+    `;
+export const RestorePageVersionDocument = gql`
+    mutation RestorePageVersion($pageId: Int!, $versionId: Int!) {
+  pages {
+    restore(pageId: $pageId, versionId: $versionId) {
+      responseResult {
+        succeeded
+        errorCode
+        slug
+        message
+      }
+    }
+  }
+}
+    `;
+export const ConvertPageEditorDocument = gql`
+    mutation ConvertPageEditor($id: Int!, $editor: String!) {
+  pages {
+    convert(id: $id, editor: $editor) {
+      responseResult {
+        succeeded
+        errorCode
+        slug
+        message
+      }
+    }
+  }
+}
+    `;
+export const RenderPageDocument = gql`
+    mutation RenderPage($id: Int!) {
+  pages {
+    render(id: $id) {
+      responseResult {
+        succeeded
+        errorCode
+        slug
+        message
+      }
     }
   }
 }
@@ -2027,6 +2323,42 @@ export function getSdk(client: GraphQLClient, withWrapper: SdkFunctionWrapper =
     },
     UpdatePage(variables: UpdatePageMutationVariables, requestHeaders?: GraphQLClientRequestHeaders, signal?: RequestInit['signal']): Promise<UpdatePageMutation> {
       return withWrapper((wrappedRequestHeaders) => client.request<UpdatePageMutation>({ document: UpdatePageDocument, variables, requestHeaders: { ...requestHeaders, ...wrappedRequestHeaders }, signal }), 'UpdatePage', 'mutation', variables);
+    },
+    GetPageHistory(variables: GetPageHistoryQueryVariables, requestHeaders?: GraphQLClientRequestHeaders, signal?: RequestInit['signal']): Promise<GetPageHistoryQuery> {
+      return withWrapper((wrappedRequestHeaders) => client.request<GetPageHistoryQuery>({ document: GetPageHistoryDocument, variables, requestHeaders: { ...requestHeaders, ...wrappedRequestHeaders }, signal }), 'GetPageHistory', 'query', variables);
+    },
+    GetPageVersion(variables: GetPageVersionQueryVariables, requestHeaders?: GraphQLClientRequestHeaders, signal?: RequestInit['signal']): Promise<GetPageVersionQuery> {
+      return withWrapper((wrappedRequestHeaders) => client.request<GetPageVersionQuery>({ document: GetPageVersionDocument, variables, requestHeaders: { ...requestHeaders, ...wrappedRequestHeaders }, signal }), 'GetPageVersion', 'query', variables);
+    },
+    GetPageTree(variables: GetPageTreeQueryVariables, requestHeaders?: GraphQLClientRequestHeaders, signal?: RequestInit['signal']): Promise<GetPageTreeQuery> {
+      return withWrapper((wrappedRequestHeaders) => client.request<GetPageTreeQuery>({ document: GetPageTreeDocument, variables, requestHeaders: { ...requestHeaders, ...wrappedRequestHeaders }, signal }), 'GetPageTree', 'query', variables);
+    },
+    ListTags(variables?: ListTagsQueryVariables, requestHeaders?: GraphQLClientRequestHeaders, signal?: RequestInit['signal']): Promise<ListTagsQuery> {
+      return withWrapper((wrappedRequestHeaders) => client.request<ListTagsQuery>({ document: ListTagsDocument, variables, requestHeaders: { ...requestHeaders, ...wrappedRequestHeaders }, signal }), 'ListTags', 'query', variables);
+    },
+    SearchTags(variables: SearchTagsQueryVariables, requestHeaders?: GraphQLClientRequestHeaders, signal?: RequestInit['signal']): Promise<SearchTagsQuery> {
+      return withWrapper((wrappedRequestHeaders) => client.request<SearchTagsQuery>({ document: SearchTagsDocument, variables, requestHeaders: { ...requestHeaders, ...wrappedRequestHeaders }, signal }), 'SearchTags', 'query', variables);
+    },
+    GetPageLinks(variables: GetPageLinksQueryVariables, requestHeaders?: GraphQLClientRequestHeaders, signal?: RequestInit['signal']): Promise<GetPageLinksQuery> {
+      return withWrapper((wrappedRequestHeaders) => client.request<GetPageLinksQuery>({ document: GetPageLinksDocument, variables, requestHeaders: { ...requestHeaders, ...wrappedRequestHeaders }, signal }), 'GetPageLinks', 'query', variables);
+    },
+    CheckConflicts(variables: CheckConflictsQueryVariables, requestHeaders?: GraphQLClientRequestHeaders, signal?: RequestInit['signal']): Promise<CheckConflictsQuery> {
+      return withWrapper((wrappedRequestHeaders) => client.request<CheckConflictsQuery>({ document: CheckConflictsDocument, variables, requestHeaders: { ...requestHeaders, ...wrappedRequestHeaders }, signal }), 'CheckConflicts', 'query', variables);
+    },
+    MovePage(variables: MovePageMutationVariables, requestHeaders?: GraphQLClientRequestHeaders, signal?: RequestInit['signal']): Promise<MovePageMutation> {
+      return withWrapper((wrappedRequestHeaders) => client.request<MovePageMutation>({ document: MovePageDocument, variables, requestHeaders: { ...requestHeaders, ...wrappedRequestHeaders }, signal }), 'MovePage', 'mutation', variables);
+    },
+    DeletePage(variables: DeletePageMutationVariables, requestHeaders?: GraphQLClientRequestHeaders, signal?: RequestInit['signal']): Promise<DeletePageMutation> {
+      return withWrapper((wrappedRequestHeaders) => client.request<DeletePageMutation>({ document: DeletePageDocument, variables, requestHeaders: { ...requestHeaders, ...wrappedRequestHeaders }, signal }), 'DeletePage', 'mutation', variables);
+    },
+    RestorePageVersion(variables: RestorePageVersionMutationVariables, requestHeaders?: GraphQLClientRequestHeaders, signal?: RequestInit['signal']): Promise<RestorePageVersionMutation> {
+      return withWrapper((wrappedRequestHeaders) => client.request<RestorePageVersionMutation>({ document: RestorePageVersionDocument, variables, requestHeaders: { ...requestHeaders, ...wrappedRequestHeaders }, signal }), 'RestorePageVersion', 'mutation', variables);
+    },
+    ConvertPageEditor(variables: ConvertPageEditorMutationVariables, requestHeaders?: GraphQLClientRequestHeaders, signal?: RequestInit['signal']): Promise<ConvertPageEditorMutation> {
+      return withWrapper((wrappedRequestHeaders) => client.request<ConvertPageEditorMutation>({ document: ConvertPageEditorDocument, variables, requestHeaders: { ...requestHeaders, ...wrappedRequestHeaders }, signal }), 'ConvertPageEditor', 'mutation', variables);
+    },
+    RenderPage(variables: RenderPageMutationVariables, requestHeaders?: GraphQLClientRequestHeaders, signal?: RequestInit['signal']): Promise<RenderPageMutation> {
+      return withWrapper((wrappedRequestHeaders) => client.request<RenderPageMutation>({ document: RenderPageDocument, variables, requestHeaders: { ...requestHeaders, ...wrappedRequestHeaders }, signal }), 'RenderPage', 'mutation', variables);
     }
   };
 }

+ 182 - 52
src/wikijs/index.ts

@@ -1,17 +1,21 @@
 import { GraphQLClient } from 'graphql-request';
-import { getSdk, Sdk } from	'./generated/graphql.js';
+import { getSdk, Sdk, PageTreeMode } from './generated/graphql.js';
+
+const REQUEST_TIMEOUT_MS = 30000;
 
 export class WikiJSClient {
   private sdk: Sdk;
   private baseUrl: string;
+  private defaultLocale: string;
 
-  constructor(endpoint: string, authToken: string) {
+  constructor(endpoint: string, authToken: string, defaultLocale: string = 'en') {
     const defaultHeaders = {
       Authorization: `Bearer ${authToken}`
     };
 
     // Remove trailing slash if present
-    this.baseUrl = endpoint.replace(/\/$/, '');
+    this.baseUrl = endpoint.replace(/\/+$/, '');
+    this.defaultLocale = defaultLocale || 'en';
 
     const endpointWithGraphql = `${this.baseUrl}/graphql`;
 
@@ -22,57 +26,74 @@ export class WikiJSClient {
     this.sdk = getSdk(client);
   }
 
-  private buildUrl(path: string, locale?: string): string {
-    // WikiJS URLs are typically: baseUrl/locale/path or baseUrl/path
-    const pathWithoutLeadingSlash = path.replace(/^\//, '');
-    if (locale && locale !== 'en') {
-      return `${this.baseUrl}/${locale}/${pathWithoutLeadingSlash}`;
+  private signal(): AbortSignal {
+    return AbortSignal.timeout(REQUEST_TIMEOUT_MS);
+  }
+
+  private buildUrl(path: string, locale?: string | null): string {
+    const pathWithoutLeadingSlash = (path || '').replace(/^\/+/, '');
+    const encodedPath = pathWithoutLeadingSlash
+      .split('/')
+      .filter(Boolean)
+      .map((seg) => encodeURIComponent(seg))
+      .join('/');
+
+    const loc = (locale || this.defaultLocale).trim();
+    // Wiki.js omits the locale segment only for the site's default locale.
+    if (loc && loc !== this.defaultLocale) {
+      return `${this.baseUrl}/${encodeURIComponent(loc)}/${encodedPath}`;
     }
-    return `${this.baseUrl}/${pathWithoutLeadingSlash}`;
+    return `${this.baseUrl}/${encodedPath}`;
   }
 
   private addUrlToPage<T extends { path?: string | null; locale?: string | null }>(page: T | null): (T & { url: string }) | null {
-    if (!page || !page.path) return null;
-    return {
-      ...page,
-      url: this.buildUrl(page.path, page.locale || undefined)
-    };
+    if (!page) return null;
+    const url = this.buildUrl(page.path || '', page.locale);
+    return { ...page, url };
   }
 
   private addUrlToPages<T extends { path?: string | null; locale?: string | null }>(pages: T[]): (T & { url: string })[] {
-    return pages.map(page => ({
+    return pages.map((page) => ({
       ...page,
-      url: this.buildUrl(page.path || '', page.locale || undefined)
+      url: this.buildUrl(page.path || '', page.locale)
     }));
   }
 
-
   async searchPages(query: string, path?: string, locale?: string) {
-    const result = await this.sdk.SearchPages({ query, path, locale });
+    const result = await this.sdk.SearchPages({ query, path, locale }, undefined, this.signal());
     const search = result.pages?.search;
     if (!search) {
       return { results: [], suggestions: [], totalHits: 0 };
     }
     const filteredResults = (search.results || []).filter((r): r is NonNullable<typeof r> => r !== null);
+    const withUrls = this.addUrlToPages(filteredResults);
     return {
-      ...search,
-      results: this.addUrlToPages(filteredResults)
+      results: withUrls,
+      suggestions: search.suggestions || [],
+      totalHits: search.totalHits || 0
     };
   }
 
-  async getAllPages({limit, locale, tags}: {limit?: number, locale?: string, tags?: string[]} = {}) {
-    const result = await this.sdk.GetAllPages({ limit, locale, tags });
-    return this.addUrlToPages(result.pages?.list || []);
+  async getAllPages({ limit, locale, tags }: { limit?: number; locale?: string; tags?: string[] } = {}) {
+    const result = await this.sdk.GetAllPages({ limit, locale, tags }, undefined, this.signal());
+    const pages = result.pages?.list || [];
+    return this.addUrlToPages(pages);
   }
 
   async getPageById(id: number) {
-    const result = await this.sdk.GetPageById({ id });
-    return this.addUrlToPage(result.pages?.single || null);
+    const result = await this.sdk.GetPageById({ id }, undefined, this.signal());
+    const page = result.pages?.single || null;
+    const withUrl = this.addUrlToPage(page);
+    if (!withUrl) return null;
+    return withUrl;
   }
 
   async getPageByPath(path: string, locale: string) {
-    const result = await this.sdk.GetPageByPath({ path, locale });
-    return this.addUrlToPage(result.pages?.singleByPath || null);
+    const result = await this.sdk.GetPageByPath({ path, locale }, undefined, this.signal());
+    const page = result.pages?.singleByPath || null;
+    const withUrl = this.addUrlToPage(page);
+    if (!withUrl) return null;
+    return withUrl;
   }
 
   async createPage(params: {
@@ -86,34 +107,143 @@ export class WikiJSClient {
     locale?: string;
     tags?: string[];
   }) {
-    const result = await this.sdk.CreatePage({
-      title: params.title,
-      content: params.content,
-      description: params.description,
-      path: params.path,
-      editor: params.editor || 'markdown',
-      isPublished: params.isPublished ?? true,
-      isPrivate: params.isPrivate ?? false,
-      locale: params.locale || 'en',
-      tags: params.tags || []
-    });
-    
-    return result.pages?.create || null;
+    const result = await this.sdk.CreatePage(
+      {
+        title: params.title,
+        content: params.content,
+        description: params.description,
+        path: params.path,
+        editor: params.editor || 'markdown',
+        isPublished: params.isPublished ?? true,
+        isPrivate: params.isPrivate ?? false,
+        locale: params.locale || 'en',
+        tags: params.tags || []
+      },
+      undefined,
+      this.signal()
+    );
+
+    const created = result.pages?.create;
+    if (!created) return null;
+    const page = created.page ? this.addUrlToPage(created.page) : null;
+    return {
+      responseResult: created.responseResult,
+      page
+    };
   }
 
   async updatePage(params: {
     id: number;
-    title: string;
-    content: string;
-    description: string;
-    editor: string;
-    isPublished: boolean;
-    isPrivate: boolean;
-    locale: string;
-    tags: string[];
-    path: string;
+    title?: string;
+    content?: string;
+    description?: string;
+    editor?: string;
+    isPublished?: boolean;
+    isPrivate?: boolean;
+    locale?: string;
+    tags?: string[];
+    path?: string;
   }) {
-    const result = await this.sdk.UpdatePage(params);
-    return result.pages?.update || null;
+    // Fetch existing page to avoid clobbering fields the caller did not supply.
+    let existing: Record<string, unknown> | null = null;
+    try {
+      existing = (await this.getPageById(params.id)) as Record<string, unknown> | null;
+    } catch {
+      existing = null;
+    }
+
+    const tagsFromExisting: string[] =
+      Array.isArray(existing?.tags) && existing.tags.length && typeof existing.tags[0] === 'object'
+        ? (existing.tags as Array<{ tag?: string }>).map((t) => t.tag).filter((t): t is string => Boolean(t))
+        : Array.isArray(existing?.tags)
+          ? (existing.tags as unknown[]).filter((t): t is string => typeof t === 'string')
+          : [];
+
+    const merged = {
+      id: params.id,
+      title: params.title ?? (existing?.title as string | undefined) ?? '',
+      content: params.content ?? (existing?.content as string | undefined) ?? '',
+      description: params.description ?? (existing?.description as string | undefined) ?? '',
+      editor: params.editor ?? (existing?.editor as string | undefined) ?? 'markdown',
+      isPublished: params.isPublished ?? (existing?.isPublished as boolean | undefined) ?? true,
+      isPrivate: params.isPrivate ?? (existing?.isPrivate as boolean | undefined) ?? false,
+      locale: params.locale ?? (existing?.locale as string | undefined) ?? this.defaultLocale,
+      tags: params.tags ?? tagsFromExisting,
+      path: params.path ?? (existing?.path as string | undefined) ?? ''
+    };
+
+    if (!merged.content || merged.content.trim().length === 0) {
+      throw new Error('content is required to update a page');
+    }
+
+    const result = await this.sdk.UpdatePage(merged, undefined, this.signal());
+    const updated = result.pages?.update;
+    if (!updated) return null;
+    const page = updated.page ? this.addUrlToPage(updated.page) : null;
+    return {
+      responseResult: updated.responseResult,
+      page
+    };
+  }
+
+  async getPageHistory(id: number, offsetPage?: number, offsetSize?: number) {
+    const result = await this.sdk.GetPageHistory({ id, offsetPage, offsetSize }, undefined, this.signal());
+    return result.pages?.history || null;
+  }
+
+  async getPageVersion(pageId: number, versionId: number) {
+    const result = await this.sdk.GetPageVersion({ pageId, versionId }, undefined, this.signal());
+    return result.pages?.version || null;
+  }
+
+  async getPageTree(params: { path?: string; parent?: number; mode: 'FOLDERS' | 'PAGES' | 'ALL'; locale: string; includeAncestors?: boolean }) {
+    const mode = (PageTreeMode as Record<string, PageTreeMode>)[params.mode.charAt(0) + params.mode.slice(1).toLowerCase()] ?? PageTreeMode.Pages;
+    const result = await this.sdk.GetPageTree({ ...params, mode }, undefined, this.signal());
+    return result.pages?.tree || null;
+  }
+
+  async listTags() {
+    const result = await this.sdk.ListTags(undefined, undefined, this.signal());
+    return result.pages?.tags || [];
+  }
+
+  async searchTags(query: string) {
+    const result = await this.sdk.SearchTags({ query }, undefined, this.signal());
+    return result.pages?.searchTags || [];
+  }
+
+  async getPageLinks(locale: string) {
+    const result = await this.sdk.GetPageLinks({ locale }, undefined, this.signal());
+    return result.pages?.links || null;
+  }
+
+  async checkConflicts(id: number, checkoutDate: string) {
+    const result = await this.sdk.CheckConflicts({ id, checkoutDate }, undefined, this.signal());
+    return result.pages?.checkConflicts ?? null;
+  }
+
+  async movePage(id: number, destinationPath: string, destinationLocale: string) {
+    const result = await this.sdk.MovePage({ id, destinationPath, destinationLocale }, undefined, this.signal());
+    return result.pages?.move || null;
+  }
+
+  async deletePage(id: number) {
+    const result = await this.sdk.DeletePage({ id }, undefined, this.signal());
+    return result.pages?.delete || null;
+  }
+
+  async restorePageVersion(pageId: number, versionId: number) {
+    const result = await this.sdk.RestorePageVersion({ pageId, versionId }, undefined, this.signal());
+    return result.pages?.restore || null;
+  }
+
+  async convertPageEditor(id: number, editor: string) {
+    const result = await this.sdk.ConvertPageEditor({ id, editor }, undefined, this.signal());
+    return result.pages?.convert || null;
+  }
+
+  async renderPage(id: number) {
+    const result = await this.sdk.RenderPage({ id }, undefined, this.signal());
+    return result.pages?.render || null;
   }
-} 
+}

+ 210 - 9
src/wikijs/queries/pages.graphql

@@ -13,6 +13,16 @@ query GetPageById($id: Int!) {
       createdAt
       updatedAt
       locale
+      editor
+      isPublished
+      isPrivate
+      render
+      toc
+      contentType
+      publishStartDate
+      publishEndDate
+      scriptCss
+      scriptJs
     }
   }
 }
@@ -30,6 +40,16 @@ query GetPageByPath($path: String!, $locale: String!) {
         tag
         title
       }
+      editor
+      isPublished
+      isPrivate
+      render
+      toc
+      contentType
+      publishStartDate
+      publishEndDate
+      scriptCss
+      scriptJs
     }
   }
 }
@@ -45,6 +65,8 @@ query GetAllPages($limit: Int, $locale: String, $tags: [String!]) {
       createdAt
       updatedAt
       tags
+      isPublished
+      isPrivate
     }
   }
 }
@@ -53,6 +75,7 @@ query SearchPages($query: String!, $path: String, $locale: String) {
   pages {
     search(query: $query, path: $path, locale: $locale) {
       results {
+        id
         title
         description
         path
@@ -93,21 +116,26 @@ mutation CreatePage(
         slug
         message
       }
+      page {
+        id
+        path
+        locale
+      }
     }
   }
 }
 
 mutation UpdatePage(
   $id: Int!
-  $title: String!
-  $content: String!
-  $description: String!
-  $editor: String!
-  $isPublished: Boolean!
-  $isPrivate: Boolean!
-  $locale: String!
-  $tags: [String]!
-  $path: String!
+  $title: String
+  $content: String
+  $description: String
+  $editor: String
+  $isPublished: Boolean
+  $isPrivate: Boolean
+  $locale: String
+  $tags: [String]
+  $path: String
 ) {
   pages {
     update(
@@ -128,6 +156,179 @@ mutation UpdatePage(
         slug
         message
       }
+      page {
+        id
+        path
+        locale
+      }
+    }
+  }
+}
+
+# -----------------------------------------------
+# EXTENDED QUERIES (Wiki.js 2.5)
+# -----------------------------------------------
+
+query GetPageHistory($id: Int!, $offsetPage: Int, $offsetSize: Int) {
+  pages {
+    history(id: $id, offsetPage: $offsetPage, offsetSize: $offsetSize) {
+      trail {
+        versionId
+        versionDate
+        authorId
+        authorName
+        actionType
+        valueBefore
+        valueAfter
+      }
+      total
+    }
+  }
+}
+
+query GetPageVersion($pageId: Int!, $versionId: Int!) {
+  pages {
+    version(pageId: $pageId, versionId: $versionId) {
+      action
+      authorId
+      authorName
+      content
+      contentType
+      createdAt
+      versionDate
+      description
+      editor
+      isPrivate
+      isPublished
+      locale
+      pageId
+      path
+      publishEndDate
+      publishStartDate
+      tags
+      title
+      versionId
+    }
+  }
+}
+
+query GetPageTree($path: String, $parent: Int, $mode: PageTreeMode!, $locale: String!, $includeAncestors: Boolean) {
+  pages {
+    tree(path: $path, parent: $parent, mode: $mode, locale: $locale, includeAncestors: $includeAncestors) {
+      id
+      path
+      depth
+      title
+      isPrivate
+      isFolder
+      privateNS
+      parent
+      pageId
+      locale
+    }
+  }
+}
+
+query ListTags {
+  pages {
+    tags {
+      id
+      tag
+      title
+      createdAt
+      updatedAt
+    }
+  }
+}
+
+query SearchTags($query: String!) {
+  pages {
+    searchTags(query: $query)
+  }
+}
+
+query GetPageLinks($locale: String!) {
+  pages {
+    links(locale: $locale) {
+      id
+      path
+      title
+      links
+    }
+  }
+}
+
+query CheckConflicts($id: Int!, $checkoutDate: Date!) {
+  pages {
+    checkConflicts(id: $id, checkoutDate: $checkoutDate)
+  }
+}
+
+# -----------------------------------------------
+# EXTENDED MUTATIONS (Wiki.js 2.5)
+# -----------------------------------------------
+
+mutation MovePage($id: Int!, $destinationPath: String!, $destinationLocale: String!) {
+  pages {
+    move(id: $id, destinationPath: $destinationPath, destinationLocale: $destinationLocale) {
+      responseResult {
+        succeeded
+        errorCode
+        slug
+        message
+      }
+    }
+  }
+}
+
+mutation DeletePage($id: Int!) {
+  pages {
+    delete(id: $id) {
+      responseResult {
+        succeeded
+        errorCode
+        slug
+        message
+      }
+    }
+  }
+}
+
+mutation RestorePageVersion($pageId: Int!, $versionId: Int!) {
+  pages {
+    restore(pageId: $pageId, versionId: $versionId) {
+      responseResult {
+        succeeded
+        errorCode
+        slug
+        message
+      }
+    }
+  }
+}
+
+mutation ConvertPageEditor($id: Int!, $editor: String!) {
+  pages {
+    convert(id: $id, editor: $editor) {
+      responseResult {
+        succeeded
+        errorCode
+        slug
+        message
+      }
+    }
+  }
+}
+
+mutation RenderPage($id: Int!) {
+  pages {
+    render(id: $id) {
+      responseResult {
+        succeeded
+        errorCode
+        slug
+        message
+      }
     }
   }
 }

+ 3 - 2
tsconfig.json

@@ -2,8 +2,8 @@
 {
 	"compilerOptions": {
 	  "target": "es2024",
-	  "module": "es2022",
-	  "moduleResolution": "bundler",
+	  "module": "NodeNext",
+	  "moduleResolution": "NodeNext",
 	  "esModuleInterop": true,
 	  "strict": true,
 	  "outDir": "build",
@@ -12,6 +12,7 @@
 	  "rootDir": "src",
 	  "forceConsistentCasingInFileNames": true,
 	  "skipLibCheck": true,
+	  "isolatedModules": true,
 	},
 	"include": ["src/**/*"],
 	"exclude": ["node_modules", "build",]