/**
* Page and block tool handlers.
* @module
*/
import type {
LogseqOperations,
BlockNode,
CreatePageResult,
Page,
FindPagesByPropertiesOptions,
FindOrphanPagesOptions,
UpdatePagePropertiesOptions,
} from "@logseq-ai/core";
import type { ToolResult } from "../types.js";
import { validate } from "../validation/index.js";
import {
searchLogseqSchema,
getPageSchema,
getPagesSchema,
getPageWithContextSchema,
listPagesSchema,
createPageSchema,
deletePageSchema,
createBlockSchema,
updateBlockSchema,
deleteBlockSchema,
queryLogseqSchema,
getCurrentGraphSchema,
getGraphStatsSchema,
findMissingPagesSchema,
findPagesByPropertiesSchema,
findOrphanPagesSchema,
updatePagePropertiesSchema,
createBlocksSchema,
} from "../validation/page-schemas.js";
export async function handlePageTools(
ops: LogseqOperations,
name: string,
args: Record<string, unknown> | undefined
): Promise<ToolResult | null> {
switch (name) {
case "search_logseq": {
const { query } = validate(searchLogseqSchema, args);
const results = await ops.search(query);
return {
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
};
}
case "get_page": {
const { pageName } = validate(getPageSchema, args);
const content = await ops.getPageContent(pageName);
if (content === null) {
return {
content: [{ type: "text", text: `Page not found: ${pageName}` }],
isError: true,
};
}
return {
content: [{ type: "text", text: content }],
};
}
case "get_pages": {
const { pageNames } = validate(getPagesSchema, args);
const result = await ops.getPages(pageNames);
return {
content: [
{
type: "text",
text: `Found ${result.found} of ${pageNames.length} pages (${result.missing} missing).\n\n${JSON.stringify(result.pages, null, 2)}`,
},
],
};
}
case "get_page_with_context": {
const { pageName } = validate(getPageWithContextSchema, args);
const context = await ops.getPageWithContext(pageName);
if (!context) {
return {
content: [{ type: "text", text: `Page not found: ${pageName}` }],
isError: true,
};
}
const lines = [
`📄 **${context.page.name}**`,
``,
`**Properties:**`,
...Object.entries(context.page.properties).map(([k, v]) => ` ${k}:: ${v}`),
``,
`**Content:**`,
context.page.content || "(empty)",
``,
`**Backlinks (${context.backlinks.length} pages link here):**`,
...context.backlinks
.slice(0, 10)
.map(
(b) =>
` • [[${b.pageName}]]: "${b.blockContent.slice(0, 80)}${b.blockContent.length > 80 ? "..." : ""}"`
),
...(context.backlinks.length > 10
? [` ... and ${context.backlinks.length - 10} more`]
: []),
``,
`**Forward Links (${context.forwardLinks.length} pages referenced):**`,
` ${context.forwardLinks
.slice(0, 20)
.map((l) => `[[${l}]]`)
.join(
", "
)}${context.forwardLinks.length > 20 ? ` ... and ${context.forwardLinks.length - 20} more` : ""}`,
];
return {
content: [{ type: "text", text: lines.join("\n") }],
};
}
case "list_pages": {
validate(listPagesSchema, args);
const pages = await ops.listPages();
const pageNames = pages.map((p) => p.name).sort();
return {
content: [{ type: "text", text: JSON.stringify(pageNames, null, 2) }],
};
}
case "create_page": {
const { pageName, content, blocks } = validate(createPageSchema, args);
// Use options object if blocks provided, otherwise use simple signature
if (blocks && blocks.length > 0) {
const result = (await ops.createPage({
pageName,
content,
blocks: blocks as BlockNode[],
})) as CreatePageResult;
return {
content: [
{
type: "text",
text: `Created page: ${result.page.name} with ${result.blocksCreated} blocks.\nRoot blocks:\n${JSON.stringify(result.rootBlocks, null, 2)}`,
},
],
};
}
// Simple case: no blocks
const page = (await ops.createPage(pageName, content)) as Page;
return {
content: [{ type: "text", text: `Created page: ${page.name}` }],
};
}
case "delete_page": {
const { pageName } = validate(deletePageSchema, args);
await ops.deletePage(pageName);
return {
content: [{ type: "text", text: `Deleted page: ${pageName}` }],
};
}
case "create_block": {
const { content, pageName, parentBlockUuid } = validate(createBlockSchema, args);
const block = await ops.createBlock({
content,
...(pageName && { pageName }),
...(parentBlockUuid && { parentBlockUuid }),
});
return {
content: [{ type: "text", text: `Created block with UUID: ${block.uuid}` }],
};
}
case "update_block": {
const { uuid, content } = validate(updateBlockSchema, args);
await ops.updateBlock({ uuid, content });
return {
content: [{ type: "text", text: `Updated block: ${uuid}` }],
};
}
case "delete_block": {
const { uuid } = validate(deleteBlockSchema, args);
await ops.deleteBlock(uuid);
return {
content: [{ type: "text", text: `Deleted block: ${uuid}` }],
};
}
case "query_logseq": {
const { query } = validate(queryLogseqSchema, args);
const results = await ops.query(query);
return {
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
};
}
case "get_current_graph": {
validate(getCurrentGraphSchema, args);
const graph = await ops.getCurrentGraph();
if (!graph) {
return {
content: [{ type: "text", text: "No graph currently open" }],
isError: true,
};
}
return {
content: [{ type: "text", text: JSON.stringify(graph, null, 2) }],
};
}
case "get_graph_stats": {
validate(getGraphStatsSchema, args);
const stats = await ops.getGraphStats();
const typeEntries = Object.entries(stats.pagesByType) as [string, number][];
const summary = [
`📊 Graph Statistics`,
``,
`Total Pages: ${stats.totalPages}`,
`Journal Pages: ${stats.journalPages}`,
`Orphan Pages: ${stats.orphanPages} (no incoming links)`,
`Missing Pages: ${stats.missingPages} (referenced but don't exist)`,
``,
`Pages by Type:`,
...typeEntries.sort((a, b) => b[1] - a[1]).map(([type, count]) => ` ${type}: ${count}`),
].join("\n");
return {
content: [{ type: "text", text: summary }],
};
}
case "find_missing_pages": {
const { minReferences } = validate(findMissingPagesSchema, args);
const minRefs = minReferences ?? 1;
const result = await ops.findMissingPages(minRefs);
if (result.total === 0) {
return {
content: [{ type: "text", text: `No missing pages found with ${minRefs}+ references.` }],
};
}
const lines = [
`🔍 Found ${result.total} missing pages (min ${minRefs} references):`,
``,
...result.missingPages.map(
(p) =>
`• **${p.name}** (${p.referenceCount} refs)\n Referenced by: ${p.referencedBy.slice(0, 5).join(", ")}${p.referencedBy.length > 5 ? ` (+${p.referencedBy.length - 5} more)` : ""}`
),
];
return {
content: [{ type: "text", text: lines.join("\n") }],
};
}
case "find_pages_by_properties": {
const validated = validate(findPagesByPropertiesSchema, args);
const result = await ops.findPagesByProperties(validated as FindPagesByPropertiesOptions);
if (result.total === 0) {
return {
content: [{ type: "text", text: "No pages found matching the criteria." }],
};
}
const lines = [
`📋 Found ${result.total} pages:`,
``,
...result.pages.map((p) => {
const propStr = Object.entries(p.properties)
.slice(0, 5)
.map(([k, v]) => `${k}: ${v}`)
.join(", ");
return `• **${p.name}**\n ${propStr}${Object.keys(p.properties).length > 5 ? " ..." : ""}`;
}),
];
return {
content: [{ type: "text", text: lines.join("\n") }],
};
}
case "find_orphan_pages": {
const validated = validate(findOrphanPagesSchema, args);
const result = await ops.findOrphanPages(validated as FindOrphanPagesOptions);
if (result.total === 0) {
return {
content: [{ type: "text", text: "No orphan pages found matching the criteria." }],
};
}
const lines = [
`🏝️ Found ${result.total} orphan pages (no incoming links):`,
``,
...result.orphanPages.map((p) => {
const typeStr = p.type ? ` (${p.type})` : "";
const journalStr = p.isJournal ? " [journal]" : "";
return `• **${p.name}**${typeStr}${journalStr}`;
}),
];
return {
content: [{ type: "text", text: lines.join("\n") }],
};
}
case "update_page_properties": {
const validated = validate(updatePagePropertiesSchema, args);
const result = await ops.updatePageProperties(validated as UpdatePagePropertiesOptions);
const propEntries = Object.entries(result.properties);
const lines = [
`✅ Updated properties on "${result.pageName}" (${result.changed} changed):`,
``,
...propEntries.map(([k, v]) => ` ${k}:: ${v}`),
];
return {
content: [{ type: "text", text: lines.join("\n") }],
};
}
case "create_blocks": {
const { pageName, blocks } = validate(createBlocksSchema, args);
const result = await ops.createBlocks(pageName, blocks as BlockNode[]);
return {
content: [
{
type: "text",
text: `Created ${result.created} blocks. Root blocks:\n${JSON.stringify(result.rootBlocks, null, 2)}`,
},
],
};
}
default:
return null;
}
}