src / toolsProvider.ts
import { tool, Tool, ToolsProviderController } from "@lmstudio/sdk";
import { z } from "zod";
import { existsSync } from "fs";
import { mkdir, readFile, writeFile } from "fs/promises";
import { isAbsolute, join } from "path";
import { configSchematics } from "./configSchematics";
import { buildNotebookXml, sanitizeFileName } from "./notebookDump";
import { OneNote } from "./onenote";
import { DocCache, sliceItems, sliceText } from "./pagination";
/**
* OneNote Manager tools. Every tool wraps a native OneNote operation. Large
* reads are returned in model-controlled pages (offset + max_chars / limit).
* Tools are returned in alphabetical order so they list alphabetically in LM Studio.
*/
interface Cfg {
timeoutSeconds: number;
markdownTimeoutSeconds: number;
defaultPageChars: number;
defaultItemLimit: number;
defaultOutputDir: string;
}
const DEFAULT_CFG: Cfg = {
timeoutSeconds: 90,
markdownTimeoutSeconds: 30,
defaultPageChars: 15000,
defaultItemLimit: 100,
defaultOutputDir: "",
};
// Cache of full documents so paging a read (offset > 0) does not re-hit OneNote.
const docCache = new DocCache<{ text: string; meta?: Record<string, any> }>(24);
const CONTENT_FORMATS = ["plain", "html", "markdown"] as const;
const PAGE_INFO_LEVELS = ["basic", "all", "selection", "file_type"] as const;
function readConfig(ctl: ToolsProviderController): Cfg {
const cfg = { ...DEFAULT_CFG };
try {
const c = ctl.getPluginConfig(configSchematics);
cfg.timeoutSeconds = c.get("timeoutSeconds") ?? cfg.timeoutSeconds;
cfg.markdownTimeoutSeconds = c.get("markdownTimeoutSeconds") ?? cfg.markdownTimeoutSeconds;
cfg.defaultPageChars = c.get("defaultPageChars") ?? cfg.defaultPageChars;
cfg.defaultItemLimit = c.get("defaultItemLimit") ?? cfg.defaultItemLimit;
cfg.defaultOutputDir = c.get("defaultOutputDir") ?? cfg.defaultOutputDir;
} catch {
/* fall back to defaults */
}
return cfg;
}
export async function toolsProvider(ctl: ToolsProviderController): Promise<Tool[]> {
const cfg = readConfig(ctl);
const onenote = new OneNote({
timeoutSeconds: cfg.timeoutSeconds,
markdownTimeoutSeconds: cfg.markdownTimeoutSeconds,
// eslint-disable-next-line no-console
onLog: (line) => console.info(`[onenote-manager] ${line}`),
});
const pageChars = () => cfg.defaultPageChars;
const itemLimit = () => cfg.defaultItemLimit;
/** Wrap an implementation so any failure is returned to the model as a clean string. */
function guard<T extends (...a: any[]) => Promise<any>>(fn: T): T {
return (async (...args: any[]) => {
try {
return await fn(...args);
} catch (error: any) {
return `Error: ${error?.message ?? String(error)}`;
}
}) as T;
}
async function pagedText(
key: string,
offset: number,
maxChars: number,
forceRefresh: boolean,
fetchFull: () => Promise<{ text: string; meta?: Record<string, any> }>,
) {
let doc = forceRefresh ? undefined : docCache.get(key);
if (!doc) {
doc = await fetchFull();
docCache.set(key, doc);
}
const out: Record<string, any> = { ok: true, ...sliceText(doc.text, offset, maxChars) };
if (offset === 0 && doc.meta) Object.assign(out, doc.meta);
return out;
}
function resolveOutputDir(outputDir?: string): string {
let dir = (outputDir && outputDir.trim()) || cfg.defaultOutputDir || ctl.getWorkingDirectory();
if (!isAbsolute(dir)) dir = join(ctl.getWorkingDirectory(), dir);
return dir;
}
const entries: Array<[string, Tool]> = [];
const add = (name: string, t: Tool) => entries.push([name, t]);
// --- discovery / reading --------------------------------------------------
add(
"health_check",
tool({
name: "health_check",
description: "Verify the local OneNote COM connection and return notebook/section counts.",
parameters: {},
implementation: guard(async () => onenote.healthCheck()),
}),
);
add(
"resolve_identifier",
tool({
name: "resolve_identifier",
description:
"Resolve a OneNote identifier (object GUID, exact hierarchy path, or unique display name) to one live object.",
parameters: {
identifier: z.string().describe("GUID, exact path (e.g. 'Personal/Quick Notes/My Section'), or unique name."),
item_type: z.enum(["", "notebook", "section_group", "section", "page"]).optional(),
},
implementation: guard(async ({ identifier, item_type }) => onenote.resolveIdentifier(identifier, item_type ?? "")),
}),
);
add(
"get_special_locations",
tool({
name: "get_special_locations",
description: "Return OneNote's local special folders: backup, unfiled, and default notebook folder.",
parameters: {},
implementation: guard(async () => onenote.getSpecialLocations()),
}),
);
add(
"list_notebooks",
tool({
name: "list_notebooks",
description: "List notebooks known to the local OneNote desktop app. Paginated with offset/limit.",
parameters: {
offset: z.number().int().min(0).optional(),
limit: z.number().int().min(1).optional(),
},
implementation: guard(async ({ offset, limit }) => {
const payload = await onenote.listNotebooks();
return { ok: true, ...sliceItems(payload.notebooks, offset ?? 0, limit ?? itemLimit()) };
}),
}),
);
add(
"list_sections",
tool({
name: "list_sections",
description: "List sections, optionally within one notebook. Paginated with offset/limit.",
parameters: {
notebook_identifier: z.string().optional(),
include_recycle_bin: z.boolean().optional(),
offset: z.number().int().min(0).optional(),
limit: z.number().int().min(1).optional(),
},
implementation: guard(async ({ notebook_identifier, include_recycle_bin, offset, limit }) => {
const payload = await onenote.listSections(notebook_identifier ?? "", include_recycle_bin ?? false);
return { ok: true, ...sliceItems(payload.sections, offset ?? 0, limit ?? itemLimit()) };
}),
}),
);
add(
"list_pages",
tool({
name: "list_pages",
description: "List pages within a section (GUID, path, or name). Paginated with offset/limit.",
parameters: {
section_identifier: z.string(),
include_recycle_bin: z.boolean().optional(),
offset: z.number().int().min(0).optional(),
limit: z.number().int().min(1).optional(),
},
implementation: guard(async ({ section_identifier, include_recycle_bin, offset, limit }) => {
const payload = await onenote.listPages(section_identifier, false, include_recycle_bin ?? false);
return { ok: true, section: payload.section, ...sliceItems(payload.pages, offset ?? 0, limit ?? itemLimit()) };
}),
}),
);
add(
"list_hierarchy",
tool({
name: "list_hierarchy",
description: "List live OneNote hierarchy objects from an optional start point. Paginated with offset/limit.",
parameters: {
start_identifier: z.string().optional(),
scope: z.enum(["self", "children", "notebooks", "sections", "pages"]).optional(),
include_recycle_bin: z.boolean().optional(),
offset: z.number().int().min(0).optional(),
limit: z.number().int().min(1).optional(),
},
implementation: guard(async ({ start_identifier, scope, include_recycle_bin, offset, limit }) => {
const payload = await onenote.listHierarchy(start_identifier ?? "", scope ?? "pages", false, include_recycle_bin ?? false);
return { ok: true, ...sliceItems(payload.items, offset ?? 0, limit ?? itemLimit()) };
}),
}),
);
add(
"get_page",
tool({
name: "get_page",
description:
"Read a page's plain text (title + object list on the first slice). Paginated by offset/max_chars; " +
"continue with next_offset until has_more is false. Prefer this over get_page_xml unless you need raw XML.",
parameters: {
page_identifier: z.string().describe("Page GUID (fastest — the 'id' from list/search), exact path, or unique name."),
page_info: z.enum(PAGE_INFO_LEVELS).optional().describe("Detail level (default 'basic'). Avoid 'all' unless you need binary."),
offset: z.number().int().min(0).optional(),
max_chars: z.number().int().min(1).optional(),
refresh: z.boolean().optional().describe("Re-read from OneNote instead of the cached copy (default false)."),
},
implementation: guard(async ({ page_identifier, page_info, offset, max_chars, refresh }) => {
const info = page_info ?? "basic";
const off = offset ?? 0;
return pagedText(`get_page:${page_identifier}:${info}`, off, max_chars ?? pageChars(), refresh ?? false, async () => {
const r = await onenote.getPage(page_identifier, info);
return { text: r.text, meta: { title: r.title, objects: r.objects, page: r.page } };
});
}),
}),
);
add(
"get_page_text",
tool({
name: "get_page_text",
description: "Return a page's plain text, paginated by offset/max_chars.",
parameters: {
page_identifier: z.string().describe("Page GUID (fastest), exact path, or unique name."),
offset: z.number().int().min(0).optional(),
max_chars: z.number().int().min(1).optional(),
refresh: z.boolean().optional(),
},
implementation: guard(async ({ page_identifier, offset, max_chars, refresh }) => {
const off = offset ?? 0;
return pagedText(`get_page_text:${page_identifier}`, off, max_chars ?? pageChars(), refresh ?? false, async () => ({
text: await onenote.getPageText(page_identifier),
}));
}),
}),
);
add(
"get_page_xml",
tool({
name: "get_page_xml",
description:
"Return a page's raw OneNote XML, paginated by offset/max_chars. Use only when you need the raw XML " +
"(object IDs, formatting, tables); for reading content prefer get_page / get_page_text.",
parameters: {
page_identifier: z.string().describe("Page GUID (fastest), exact path, or unique name."),
page_info: z.enum(PAGE_INFO_LEVELS).optional(),
offset: z.number().int().min(0).optional(),
max_chars: z.number().int().min(1).optional(),
refresh: z.boolean().optional(),
},
implementation: guard(async ({ page_identifier, page_info, offset, max_chars, refresh }) => {
const info = page_info ?? "basic";
const off = offset ?? 0;
return pagedText(`get_page_xml:${page_identifier}:${info}`, off, max_chars ?? pageChars(), refresh ?? false, async () => ({
text: await onenote.getPageXmlContent(page_identifier, info),
}));
}),
}),
);
add(
"get_page_objects",
tool({
name: "get_page_objects",
description:
"List a page's content objects (outlines, images, tables, attachments, ink) with object/callback IDs. " +
"Paginated with offset/limit.",
parameters: {
page_identifier: z.string(),
offset: z.number().int().min(0).optional(),
limit: z.number().int().min(1).optional(),
},
implementation: guard(async ({ page_identifier, offset, limit }) => {
const payload = await onenote.getPageObjects(page_identifier);
return { ok: true, ...sliceItems(payload.objects, offset ?? 0, limit ?? itemLimit()) };
}),
}),
);
add(
"get_binary_content",
tool({
name: "get_binary_content",
description:
"Read binary page content (base64) by callback ID from get_page_objects. The base64 string is paginated " +
"by offset/max_chars; concatenate slices in order.",
parameters: {
page_identifier: z.string(),
callback_id: z.string(),
offset: z.number().int().min(0).optional(),
max_chars: z.number().int().min(1).optional(),
refresh: z.boolean().optional(),
},
implementation: guard(async ({ page_identifier, callback_id, offset, max_chars, refresh }) => {
const off = offset ?? 0;
const out = await pagedText(
`get_binary_content:${page_identifier}:${callback_id}`,
off,
max_chars ?? Math.max(pageChars(), 8000),
refresh ?? false,
async () => ({ text: await onenote.getBinaryContent(page_identifier, callback_id) }),
);
out.encoding = "base64";
return out;
}),
}),
);
add(
"search_pages",
tool({
name: "search_pages",
description:
"Search page text using OneNote's search index (fast). Scope with start_identifier when possible. Set " +
"include_unindexed=true only as a last resort — it loads every page in scope and can slow OneNote down.",
parameters: {
query: z.string(),
start_identifier: z.string().optional().describe("Recommended scope: notebook/section GUID, path, or name."),
max_results: z.number().int().min(1).optional(),
include_snippets: z.boolean().optional(),
include_unindexed: z.boolean().optional().describe("Last-resort live scan (default false)."),
include_recycle_bin: z.boolean().optional(),
offset: z.number().int().min(0).optional(),
limit: z.number().int().min(1).optional(),
},
implementation: guard(async (a) => {
const payload = await onenote.searchPages(
a.query,
a.start_identifier ?? "",
a.max_results ?? 20,
a.include_snippets ?? true,
a.include_unindexed ?? false,
a.include_recycle_bin ?? false,
);
return { ok: true, search_backend: payload.search_backend, ...sliceItems(payload.pages, a.offset ?? 0, a.limit ?? itemLimit()) };
}),
}),
);
add(
"find_meta",
tool({
name: "find_meta",
description: "Find pages or objects with a matching OneNote meta name. Paginated with offset/limit.",
parameters: {
start_identifier: z.string().optional(),
name: z.string(),
include_unindexed: z.boolean().optional(),
offset: z.number().int().min(0).optional(),
limit: z.number().int().min(1).optional(),
},
implementation: guard(async ({ start_identifier, name, include_unindexed, offset, limit }) => {
const payload = await onenote.findMeta(start_identifier ?? "", name, include_unindexed ?? true);
return { ok: true, ...sliceItems(payload.items, offset ?? 0, limit ?? itemLimit()) };
}),
}),
);
add(
"find_references_to_hyperlink",
tool({
name: "find_references_to_hyperlink",
description:
"Scan pages for all references to a given hyperlink URL (onenote:, http, https, mailto). Returns each page's path and ID plus the anchor object IDs found on that page.",
parameters: {
hyperlink_url: z.string().describe("The exact hyperlink URL to search for."),
start_identifier: z.string().optional().describe("Recommended scope: notebook/section GUID, path, or name."),
include_recycle_bin: z.boolean().optional(),
max_snippet_chars: z.number().optional().default(100).describe("Maximum characters per text snippet (0 = unlimited paragraph text)."),
},
implementation: guard(async ({ hyperlink_url, start_identifier, include_recycle_bin, max_snippet_chars }) =>
onenote.findReferencesToHyperlink(hyperlink_url, start_identifier ?? "", include_recycle_bin ?? false, max_snippet_chars ?? 100),
),
}),
);
add(
"replace_page_text",
tool({
name: "replace_page_text",
description:
"Find all occurrences of searchText on a page and replace them with replacementText. Supports plain/html/markdown input for the replacement text; optionally preserves existing character formatting (bold/italic/etc.) from matched nodes.",
parameters: {
page_identifier: z.string(),
search_text: z.string().describe("Exact substring to find inside <one:T> elements on the page."),
replacement_text: z.string().describe("Text to replace each match with. Interpreted as plain/html/markdown per content_format."),
content_format: z.enum(CONTENT_FORMATS).optional(),
preserve_formatting: z.boolean().optional(),
},
implementation: guard(
async ({ page_identifier, search_text, replacement_text, content_format, preserve_formatting }) =>
onenote.replacePageText(
page_identifier,
search_text,
replacement_text,
content_format ?? "plain",
preserve_formatting ?? true,
),
),
}),
);
add(
"get_hyperlink",
tool({
name: "get_hyperlink",
description: "Return a OneNote client link (onenote:) or a web link for an object.",
parameters: {
object_identifier: z.string(),
page_content_object_id: z.string().optional(),
web: z.boolean().optional(),
},
implementation: guard(async ({ object_identifier, page_content_object_id, web }) =>
onenote.getHyperlink(object_identifier, page_content_object_id ?? "", web ?? false),
),
}),
);
add(
"get_parent",
tool({
name: "get_parent",
description: "Return the parent object ID of a notebook hierarchy object.",
parameters: { object_identifier: z.string() },
implementation: guard(async ({ object_identifier }) => onenote.getParent(object_identifier)),
}),
);
// --- new: table + duplicate + save-to-file --------------------------------
add(
"get_page_table_xml",
tool({
name: "get_page_table_xml",
description:
"Get the raw OneNote XML of a single table on a page. Identify the table by its objectID (from " +
"get_page_objects or the page XML) OR by its 1-based position on the page (e.g. '1' = first table). " +
"Returns table_xml and table_count.",
parameters: {
page_identifier: z.string().describe("Page GUID, exact path, or unique name."),
table_id: z.string().describe("Table objectID, containing outline-element objectID, or 1-based index."),
},
implementation: guard(async ({ page_identifier, table_id }) => onenote.getPageTableXml(page_identifier, table_id)),
}),
);
add(
"replace_page_table_xml",
tool({
name: "replace_page_table_xml",
description:
"Replace a single table on a page with new OneNote table XML. Identify the table the same way as " +
"get_page_table_xml (objectID or 1-based index). Provide a complete <one:Table>...</one:Table> element " +
"as table_xml (typically a modified copy of what get_page_table_xml returned).",
parameters: {
page_identifier: z.string(),
table_id: z.string().describe("Table objectID, containing outline-element objectID, or 1-based index."),
table_xml: z.string().describe("Replacement <one:Table> ... </one:Table> XML."),
},
implementation: guard(async ({ page_identifier, table_id, table_xml }) =>
onenote.replacePageTableXml(page_identifier, table_id, table_xml),
),
}),
);
add(
"insert_row_into_table",
tool({
name: "insert_row_into_table",
description:
"Insert a new row into a table, providing the text for each column. Existing table/cell formatting is " +
"preserved by cloning an existing row's style. Identify the table by objectID or 1-based index (same as " +
"get_page_table_xml).",
parameters: {
page_identifier: z.string(),
table_id: z.string().describe("Table objectID, containing outline-element objectID, or 1-based index."),
values: z.array(z.string()).describe("Cell text, left to right. Missing columns are left blank."),
index: z
.number()
.int()
.min(1)
.optional()
.describe("1-based position for the new row (1 = top). Default: append to the end."),
template_row_index: z
.number()
.int()
.min(1)
.optional()
.describe("1-based existing row whose formatting to copy (default: the last row)."),
},
implementation: guard(async ({ page_identifier, table_id, values, index, template_row_index }) =>
onenote.insertRowIntoTable(page_identifier, table_id, values, index, template_row_index),
),
}),
);
add(
"replace_table_cell_text",
tool({
name: "replace_table_cell_text",
description:
"Replace the text of a single table cell by its 1-based row and column position. Cell formatting is preserved.",
parameters: {
page_identifier: z.string(),
table_id: z.string().describe("Table objectID, containing outline-element objectID, or 1-based index."),
row: z.number().int().min(1).describe("1-based row number."),
column: z.number().int().min(1).describe("1-based column number."),
text: z.string().describe("New cell text (plain text; newlines become line breaks)."),
},
implementation: guard(async ({ page_identifier, table_id, row, column, text }) =>
onenote.replaceTableCellText(page_identifier, table_id, row, column, text),
),
}),
);
add(
"delete_row_from_table",
tool({
name: "delete_row_from_table",
description:
"Delete a row from a table, either by its 1-based row_index OR by matching the text of the row's first " +
"column (first_column_text, case-insensitive). If both are given, first_column_text takes priority.",
parameters: {
page_identifier: z.string(),
table_id: z.string().describe("Table objectID, containing outline-element objectID, or 1-based index."),
row_index: z.number().int().min(1).optional().describe("1-based row to delete."),
first_column_text: z.string().optional().describe("Delete the first row whose first-column text matches this."),
},
implementation: guard(async ({ page_identifier, table_id, row_index, first_column_text }) =>
onenote.deleteRowFromTable(page_identifier, table_id, row_index, first_column_text),
),
}),
);
add(
"generate_onenote_hyperlinks_xml",
tool({
name: "generate_onenote_hyperlinks_xml",
description:
"Generate (but do not insert) OneNote hyperlinks to multiple pages. Each item specifies a target page and its visible text. " +
"Returns an array of results, each with inline_html (for append_to_page with content_format 'html'), plus one_t_xml / one_oe_xml for raw " +
"insertion into a page, table cell, or outline via other commands. Supports bold/italic/underline/strikethrough per item.",
parameters: {
items: z
.array(
z.object({
target_page_identifier: z.string().describe("Target page GUID, exact path, or unique name."),
text: z.string().describe("The visible hyperlink text for this item."),
bold: z.boolean().optional(),
italic: z.boolean().optional(),
underline: z.boolean().optional(),
strikethrough: z.boolean().optional(),
}),
)
.min(1)
.describe("Hyperlink items to generate, in order."),
web: z.boolean().optional().describe("Return a web (https) link instead of a onenote: client link for all items."),
},
implementation: guard(async ({ items, web }) =>
onenote.generateOneNoteHyperlinksXml(
items.map((it: { target_page_identifier: string; text: string; bold?: boolean; italic?: boolean; underline?: boolean; strikethrough?: boolean }) => ({
targetPageIdentifier: it.target_page_identifier,
text: it.text,
formatting: { bold: it.bold, italic: it.italic, underline: it.underline, strikethrough: it.strikethrough },
})),
web ?? false,
),
),
}),
);
add(
"generate_todo_tag_xml",
tool({
name: "generate_todo_tag_xml",
description:
"Generate (but do not insert) the XML for OneNote's 'To Do' checkbox tag. Returns tag_def_xml (a page-level " +
"one:TagDef to add once if the page has none), tag_xml (the one:Tag for the start of a one:OE), and — if text " +
"is given — a ready one_oe_xml. Insert with update_page_xml or the outline insert commands.",
parameters: {
text: z.string().optional().describe("Optional text for the to-do item; returns a ready one:OE if provided."),
completed: z.boolean().optional().describe("Whether the checkbox is checked (default false)."),
tag_index: z.number().int().min(0).optional().describe("TagDef index to use (default 0)."),
},
implementation: guard(async ({ text, completed, tag_index }) =>
onenote.generateTodoTagXml(text, completed ?? false, tag_index ?? 0),
),
}),
);
add(
"get_page_outline_xml",
tool({
name: "get_page_outline_xml",
description:
"Get the raw XML of a page Outline object by its objectID or 1-based index (1 = first outline). Returns " +
"outline_xml and outline_count.",
parameters: {
page_identifier: z.string().describe("Page GUID, exact path, or unique name."),
outline_id: z.string().describe("Outline objectID, or 1-based index on the page."),
},
implementation: guard(async ({ page_identifier, outline_id }) => onenote.getPageOutlineXml(page_identifier, outline_id)),
}),
);
add(
"insert_page_outline_xml",
tool({
name: "insert_page_outline_xml",
description:
"Insert an Outline into a page, placed after all existing outlines — either at the far bottom or far right. " +
"Provide a <one:Outline>…</one:Outline> element (or loose one:OE content, which is wrapped in an outline). " +
"The outline's position is set automatically and fresh object IDs are assigned.",
parameters: {
page_identifier: z.string().describe("Page GUID, exact path, or unique name."),
outline_xml: z.string().describe("The one:Outline XML (or one:OE content) to insert."),
position: z.enum(["bottom", "right"]).optional().describe("Where to place it relative to existing outlines (default 'bottom')."),
},
implementation: guard(async ({ page_identifier, outline_xml, position }) =>
onenote.insertPageOutlineXml(page_identifier, outline_xml, position ?? "bottom"),
),
}),
);
add(
"duplicate_page_outline",
tool({
name: "duplicate_page_outline",
description:
"Duplicate an existing page Outline (by objectID or 1-based index) and place the copy after all outlines — " +
"at the far bottom or far right. OneNote assigns fresh object IDs to the copy.",
parameters: {
page_identifier: z.string().describe("Page GUID, exact path, or unique name."),
outline_id: z.string().describe("Outline objectID, or 1-based index on the page."),
position: z.enum(["bottom", "right"]).optional().describe("Where to place the copy (default 'bottom')."),
},
implementation: guard(async ({ page_identifier, outline_id, position }) =>
onenote.duplicatePageOutline(page_identifier, outline_id, position ?? "bottom"),
),
}),
);
add(
"save_page_outline_xml_to_file",
tool({
name: "save_page_outline_xml_to_file",
description:
"Read one Outline's raw XML from a page and save it to a file. Identify the outline by its objectID or " +
"1-based index (same as get_page_outline_xml). The file is named after the page title with an " +
"_outline_<index>.xml extension unless file_name is given.",
parameters: {
page_identifier: z.string().describe("Page GUID, exact path, or unique name."),
outline_id: z.string().describe("Outline objectID, or 1-based index on the page."),
output_dir: z.string().optional().describe("Directory to write into (on the OneNote machine)."),
file_name: z.string().optional().describe("Override file name (without directory). '.xml' is added if missing."),
overwrite: z.boolean().optional(),
},
implementation: guard(async ({ page_identifier, outline_id, output_dir, file_name, overwrite }) => {
const resolved = await onenote.resolveIdentifier(page_identifier, "page");
const page = resolved.item;
const result = await onenote.getPageOutlineXml(page.id, outline_id);
const dir = resolveOutputDir(output_dir);
await mkdir(dir, { recursive: true });
let name = (file_name && file_name.trim()) || sanitizeFileName(String(page.name ?? "page")) + "_outline_" + result.outline_count;
if (!/\.xml$/i.test(name)) name += ".xml";
name = sanitizeFileName(name.replace(/\.xml$/i, "")) + ".xml";
const filePath = join(dir, name);
if (existsSync(filePath) && !overwrite) {
throw new Error(`File already exists: ${filePath}. Pass overwrite=true to replace it.`);
}
await writeFile(filePath, result.outline_xml, "utf-8");
return { ok: true, file: filePath, page: page.name, page_id: page.id, outline_id: result.outline_id, bytes: Buffer.byteLength(result.outline_xml, "utf-8") };
}),
}),
);
add(
"insert_page_outline_xml_from_file",
tool({
name: "insert_page_outline_xml_from_file",
description:
"Read an Outline's XML from a file and insert it into a page, placed after all existing outlines — " +
"either at the far bottom or far right. The outline's position is set automatically and fresh object IDs " +
"are assigned.",
parameters: {
page_identifier: z.string().describe("Page GUID, exact path, or unique name."),
file_path: z.string().describe("Absolute path to a .xml file containing the one:Outline XML (or one:OE content)."),
position: z.enum(["bottom", "right"]).optional().describe("Where to place it relative to existing outlines (default 'bottom')."),
},
implementation: guard(async ({ page_identifier, file_path, position }) => {
const resolved = await onenote.resolveIdentifier(page_identifier, "page");
const page = resolved.item;
if (!existsSync(file_path)) throw new Error(`File not found: ${file_path}`);
const outlineXml = await readFile(file_path, "utf-8");
return onenote.insertPageOutlineXml(page.id, outlineXml, position ?? "bottom");
}),
}),
);
add(
"duplicate_page",
tool({
name: "duplicate_page",
description:
"Duplicate a page (e.g. copy a template) into a new page. By default the copy is placed in the source " +
"page's own section; optionally specify a target section and/or a new title. OneNote assigns fresh object IDs.",
parameters: {
page_identifier: z.string().describe("Source page GUID, exact path, or unique name."),
target_section_identifier: z.string().optional().describe("Destination section (default: same section as the source)."),
new_title: z.string().optional().describe("Title for the copy (default: the source page's title)."),
},
implementation: guard(async ({ page_identifier, target_section_identifier, new_title }) =>
onenote.duplicatePage(page_identifier, target_section_identifier ?? "", new_title),
),
}),
);
add(
"save_page_xml_to_file",
tool({
name: "save_page_xml_to_file",
description:
"Read one page and save its raw OneNote XML to a file. The file is named after the page title (with a " +
".xml extension) unless file_name is given.",
parameters: {
page_identifier: z.string().describe("Page GUID, exact path, or unique name."),
output_dir: z.string().optional().describe("Directory to write into (on the OneNote machine)."),
file_name: z.string().optional().describe("Override file name (without directory). '.xml' is added if missing."),
page_info: z.enum(PAGE_INFO_LEVELS).optional().describe("Detail level (default 'basic'). 'all' includes binary."),
overwrite: z.boolean().optional(),
},
implementation: guard(async ({ page_identifier, output_dir, file_name, page_info, overwrite }) => {
const info = page_info ?? "basic";
const resolved = await onenote.resolveIdentifier(page_identifier, "page");
const page = resolved.item;
const xml = await onenote.getPageXmlContent(page.id, info);
const dir = resolveOutputDir(output_dir);
await mkdir(dir, { recursive: true });
let name = (file_name && file_name.trim()) || sanitizeFileName(String(page.name ?? "page"));
if (!/\.xml$/i.test(name)) name += ".xml";
name = sanitizeFileName(name.replace(/\.xml$/i, "")) + ".xml";
const filePath = join(dir, name);
if (existsSync(filePath) && !overwrite) {
throw new Error(`File already exists: ${filePath}. Pass overwrite=true to replace it.`);
}
await writeFile(filePath, xml, "utf-8");
return { ok: true, file: filePath, page: page.name, page_id: page.id, bytes: Buffer.byteLength(xml, "utf-8") };
}),
}),
);
add(
"save_all_notebook_pages_xml_to_file",
tool({
name: "save_all_notebook_pages_xml_to_file",
description:
"Read an entire notebook (by name) and save the raw OneNote XML of every page to a single file, " +
"preserving the section-group / section / page structure. The file is named after the notebook with a " +
".xml extension. NOTE: this reads every page (GetPageContent), which loads them into OneNote and can make " +
"OneNote sluggish for very large notebooks — restart OneNote afterward if needed.",
parameters: {
notebook_name: z.string().describe("Notebook name (or GUID/path)."),
output_dir: z.string().optional().describe("Directory to write into (on the OneNote machine)."),
page_info: z.enum(PAGE_INFO_LEVELS).optional().describe("Page detail level (default 'basic')."),
overwrite: z.boolean().optional(),
},
implementation: guard(async ({ notebook_name, output_dir, page_info, overwrite }, { status, warn, signal }) => {
const info = page_info ?? "basic";
const resolved = await onenote.resolveIdentifier(notebook_name, "notebook");
const notebook = resolved.item;
if (!notebook?.id) throw new Error(`Could not resolve notebook '${notebook_name}'.`);
status?.(`Reading structure of '${notebook.name ?? notebook_name}'...`);
const listed = await onenote.listHierarchy(notebook.id, "pages", false, false);
const items: any[] = listed.items ?? [];
const { xml, stats } = await buildNotebookXml(
{ id: notebook.id, name: notebook.name, path: notebook.path },
items,
async (pageId) => onenote.pageXml(pageId, info),
{
onProgress: (done, total, name) => status?.(`Reading page ${done}/${total}: ${name}`),
isAborted: () => signal?.aborted ?? false,
},
);
const dir = resolveOutputDir(output_dir);
await mkdir(dir, { recursive: true });
const fileName = `${sanitizeFileName(String(notebook.name ?? notebook_name))}.xml`;
const filePath = join(dir, fileName);
if (existsSync(filePath) && !overwrite) {
throw new Error(`File already exists: ${filePath}. Pass overwrite=true to replace it.`);
}
await writeFile(filePath, xml, "utf-8");
if (stats.errors > 0) warn?.(`${stats.errors} page(s) could not be read; placeholders were written.`);
return {
ok: true,
file: filePath,
notebook: notebook.name ?? notebook_name,
pages_written: stats.pages,
sections: stats.sections,
section_groups: stats.section_groups,
page_read_errors: stats.errors,
bytes: Buffer.byteLength(xml, "utf-8"),
};
}),
}),
);
// --- creation & structural edits ------------------------------------------
add(
"open_hierarchy",
tool({
name: "open_hierarchy",
description: "Open (or create) a notebook / section group / section by path. Set create_type to create if missing.",
parameters: {
path: z.string(),
relative_to_identifier: z.string().optional(),
create_type: z.enum(["none", "notebook", "folder", "section_group", "section"]).optional(),
},
implementation: guard(async ({ path, relative_to_identifier, create_type }) =>
onenote.openHierarchy(path, relative_to_identifier ?? "", create_type ?? "none"),
),
}),
);
add(
"create_notebook",
tool({
name: "create_notebook",
description: "Create a local notebook folder and open it in OneNote.",
parameters: {
name_or_path: z.string().describe("Notebook name, or an absolute folder path."),
base_folder: z.string().optional(),
},
implementation: guard(async ({ name_or_path, base_folder }) => onenote.createNotebook(name_or_path, base_folder ?? "")),
}),
);
add(
"create_section",
tool({
name: "create_section",
description: "Create a section under a notebook or section group.",
parameters: { parent_identifier: z.string(), section_name: z.string() },
implementation: guard(async ({ parent_identifier, section_name }) => onenote.createSection(parent_identifier, section_name)),
}),
);
add(
"create_section_group",
tool({
name: "create_section_group",
description: "Create a section group under a notebook or another section group.",
parameters: { parent_identifier: z.string(), group_name: z.string() },
implementation: guard(async ({ parent_identifier, group_name }) => onenote.createSectionGroup(parent_identifier, group_name)),
}),
);
add(
"create_page",
tool({
name: "create_page",
description: "Create a page in a section. content_format accepts plain, html, or markdown (markdown needs the OneMore add-in).",
parameters: {
section_identifier: z.string(),
title: z.string(),
content: z.string().optional(),
content_format: z.enum(CONTENT_FORMATS).optional(),
new_page_style: z.enum(["default", "blank_with_title", "blank_no_title"]).optional(),
},
implementation: guard(async ({ section_identifier, title, content, content_format, new_page_style }) =>
onenote.createPage(section_identifier, title, content ?? "", content_format ?? "plain", new_page_style ?? "blank_with_title"),
),
}),
);
add(
"update_page_title",
tool({
name: "update_page_title",
description: "Update a page's title.",
parameters: { page_identifier: z.string(), title: z.string() },
implementation: guard(async ({ page_identifier, title }) => onenote.updatePageTitle(page_identifier, title)),
}),
);
add(
"add_outline_to_page",
tool({
name: "add_outline_to_page",
description:
"Add a new outline block to a page (creates a new floating outline). content_format accepts plain, html, " +
"or markdown. To add content to an EXISTING outline instead, use add_content_to_page_outline.",
parameters: {
page_identifier: z.string(),
content: z.string(),
content_format: z.enum(CONTENT_FORMATS).optional(),
x: z.number().optional(),
y: z.number().optional(),
},
implementation: guard(async ({ page_identifier, content, content_format, x, y }) =>
onenote.addOutlineToPage(page_identifier, content, content_format ?? "plain", x, y),
),
}),
);
add(
"add_content_to_page_outline",
tool({
name: "add_content_to_page_outline",
description:
"Add content to an outline on a page (appended to that outline). content_format accepts plain, html, or " +
"markdown. Identify the outline by objectID or 1-based index; if outline_id is omitted the first outline " +
"is used, and if the page has no outline one is created automatically.",
parameters: {
page_identifier: z.string(),
outline_id: z
.string()
.optional()
.describe("Outline objectID or 1-based index. Omit to use the first outline (or create one if none exist)."),
content: z.string(),
content_format: z.enum(CONTENT_FORMATS).optional(),
},
implementation: guard(async ({ page_identifier, outline_id, content, content_format }) =>
onenote.addContentToPageOutline(page_identifier, outline_id, content, content_format ?? "plain"),
),
}),
);
add(
"add_todo_items_to_page_outline",
tool({
name: "add_todo_items_to_page_outline",
description:
"Add one or more To Do (checkbox) items to an outline on a page. Creates the page's To Do TagDef if none " +
"exists. Identify the outline by objectID or 1-based index; if outline_id is omitted the first outline is " +
"used, and if the page has no outline one is created automatically. Each item's text is interpreted per " +
"content_format (plain, html, or markdown), so items can include hyperlinks or inline formatting (e.g. a " +
"onenote: link from generate_onenote_hyperlinks_xml, or markdown '[text](url)'). Each item may set its own " +
"indent_level for sub-items (0 = top level, 1 = indented under the previous item, etc.; an item can go at " +
"most one level deeper than the one before it). By default the items are appended (continuing the list); " +
"pass position (1-based) to insert the batch between existing items at the first item's indent level.",
parameters: {
page_identifier: z.string(),
outline_id: z
.string()
.optional()
.describe("Outline objectID or 1-based index. Omit to use the first outline (or create one if none exist)."),
content_format: z
.enum(["plain", "html", "markdown"])
.optional()
.describe(
"How each item's text is interpreted: 'plain' (escaped), 'html' (inline HTML — hyperlinks/formatting " +
"kept), or 'markdown'. Block structure collapses to line breaks. Default 'plain'.",
),
items: z
.array(
z.object({
text: z.string().describe("The to-do item text (interpreted per content_format)."),
completed: z.boolean().optional().describe("Whether the checkbox is checked (default false)."),
indent_level: z
.number()
.int()
.min(0)
.optional()
.describe("Indentation depth for this sub-item (0 = top level; default 0)."),
}),
)
.min(1)
.describe("The to-do items to add, in order."),
position: z
.number()
.int()
.min(1)
.optional()
.describe(
"1-based position for the first item among items at its indent level (1 = before the first). " +
"Default: append.",
),
},
implementation: guard(async ({ page_identifier, outline_id, content_format, items, position }) =>
onenote.addTodoItemsToPageOutline(
page_identifier,
outline_id,
items.map((it: { text: string; completed?: boolean; indent_level?: number }) => ({
text: it.text,
completed: it.completed,
indentLevel: it.indent_level,
})),
content_format ?? "plain",
position,
),
),
}),
);
add(
"add_image_to_page",
tool({
name: "add_image_to_page",
description: "Add a local image file to a page. Missing width/height are inferred from the image's native size.",
parameters: {
page_identifier: z.string(),
image_path: z.string().describe("Absolute path to a local image (on the machine running OneNote)."),
image_format: z.string().optional(),
x: z.number().optional(),
y: z.number().optional(),
width: z.number().optional(),
height: z.number().optional(),
},
implementation: guard(async ({ page_identifier, image_path, image_format, x, y, width, height }) =>
onenote.addImageToPage(page_identifier, image_path, image_format ?? "", x ?? 36.0, y ?? 120.0, width, height),
),
}),
);
add(
"replace_entire_page_body",
tool({
name: "replace_entire_page_body",
description:
"Delete a page's existing content objects and write new body content. Optionally update the title. Destructive.",
parameters: {
page_identifier: z.string(),
content: z.string(),
title: z.string().optional(),
content_format: z.enum(CONTENT_FORMATS).optional(),
},
implementation: guard(async ({ page_identifier, content, title, content_format }) =>
onenote.replaceEntirePageBody(page_identifier, content, title, content_format ?? "plain"),
),
}),
);
add(
"delete_page_content",
tool({
name: "delete_page_content",
description: "Delete one deletable content object from a page by its object ID (from get_page_objects). Destructive.",
parameters: { page_identifier: z.string(), object_id: z.string() },
implementation: guard(async ({ page_identifier, object_id }) => onenote.deletePageContent(page_identifier, object_id)),
}),
);
add(
"delete_hierarchy",
tool({
name: "delete_hierarchy",
description:
"Delete a notebook, section group, section, or page. Goes to the recycle bin unless permanently=true. Destructive.",
parameters: { object_identifier: z.string(), permanently: z.boolean().optional() },
implementation: guard(async ({ object_identifier, permanently }) => onenote.deleteHierarchy(object_identifier, permanently ?? false)),
}),
);
add(
"update_page_xml",
tool({
name: "update_page_xml",
description: "Advanced: submit raw OneNote page XML to UpdatePageContent. Use force=true to override conflicts.",
parameters: { xml: z.string(), force: z.boolean().optional() },
implementation: guard(async ({ xml, force }) => onenote.updatePageXml(xml, force ?? false)),
}),
);
add(
"update_hierarchy_xml",
tool({
name: "update_hierarchy_xml",
description: "Advanced: submit raw OneNote hierarchy XML to UpdateHierarchy.",
parameters: { xml: z.string() },
implementation: guard(async ({ xml }) => onenote.updateHierarchyXml(xml)),
}),
);
// --- file & app control ---------------------------------------------------
add(
"publish_object",
tool({
name: "publish_object",
description: "Export a notebook, section, or page to a local file (PDF, Word, XPS, HTML, .one, etc.).",
parameters: {
object_identifier: z.string(),
target_path: z.string().describe("Output file path on the OneNote machine."),
format: z.enum(["pdf", "xps", "word", "docx", "doc", "one", "onepkg", "mhtml", "mht", "html", "emf", "one2007"]).optional(),
overwrite: z.boolean().optional(),
},
implementation: guard(async ({ object_identifier, target_path, format, overwrite }) =>
onenote.publishObject(object_identifier, target_path, format ?? "pdf", overwrite ?? false),
),
}),
);
add(
"navigate_to",
tool({
name: "navigate_to",
description: "Open/focus a OneNote object in the desktop app.",
parameters: {
object_identifier: z.string(),
page_content_object_id: z.string().optional(),
new_window: z.boolean().optional(),
},
implementation: guard(async ({ object_identifier, page_content_object_id, new_window }) =>
onenote.navigateTo(object_identifier, page_content_object_id ?? "", new_window ?? false),
),
}),
);
add(
"navigate_to_url",
tool({
name: "navigate_to_url",
description: "Open a OneNote URL (onenote:) in the desktop app.",
parameters: { url: z.string(), new_window: z.boolean().optional() },
implementation: guard(async ({ url, new_window }) => onenote.navigateToUrl(url, new_window ?? false)),
}),
);
add(
"sync_hierarchy",
tool({
name: "sync_hierarchy",
description: "Ask OneNote to sync a notebook hierarchy object.",
parameters: { object_identifier: z.string() },
implementation: guard(async ({ object_identifier }) => onenote.syncHierarchy(object_identifier)),
}),
);
add(
"close_notebook",
tool({
name: "close_notebook",
description: "Close a notebook in the desktop OneNote app.",
parameters: { notebook_identifier: z.string(), force: z.boolean().optional() },
implementation: guard(async ({ notebook_identifier, force }) => onenote.closeNotebook(notebook_identifier, force ?? false)),
}),
);
add(
"merge_sections",
tool({
name: "merge_sections",
description: "Merge one section into another. Destructive — the source section's pages move into the destination.",
parameters: { source_section_identifier: z.string(), destination_section_identifier: z.string() },
implementation: guard(async ({ source_section_identifier, destination_section_identifier }) =>
onenote.mergeSections(source_section_identifier, destination_section_identifier),
),
}),
);
add(
"set_filing_location",
tool({
name: "set_filing_location",
description: "Set OneNote's local filing location for email, web clips, printouts, and similar content.",
parameters: {
filing_location: z.enum(["email", "contacts", "tasks", "meetings", "web_content", "printouts"]),
filing_location_type: z.enum(["named_section_new_page", "current_section_new_page", "current_page", "named_page"]),
section_or_page_identifier: z.string(),
},
implementation: guard(async ({ filing_location, filing_location_type, section_or_page_identifier }) =>
onenote.setFilingLocation(filing_location, filing_location_type, section_or_page_identifier),
),
}),
);
// Return tools sorted alphabetically by name so they list alphabetically in LM Studio.
entries.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
return entries.map(([, t]) => t);
}