Project Files
src / tools / rewrite_doc.ts
/**
* rewrite_doc — replace the body of an existing notes document in-place.
* Preserves filename, title, and original created date.
* Optionally replaces tags; if omitted, existing tags are kept.
*/
import { tool, type Tool, type ToolsProviderController } from "@lmstudio/sdk";
// @ts-ignore — zod/lib re-export chain breaks with NodeNext; runtime is fine
import { z } from "zod";
import path from "node:path";
import fs from "node:fs";
import { globalConfigSchematics } from "../config.js";
import { buildFrontmatter } from "../helpers/frontmatter.js";
// @ts-ignore — typed via core-bundle.d.mts
import { formatToolMetaBlock } from "../core-bundle.mjs";
/** Extract a scalar YAML field from a frontmatter block (best-effort). */
function extractFrontmatterField(fm: string, key: string): string | undefined {
const m = fm.match(new RegExp(`^${key}:\\s*"?([^"\\n]+)"?`, "m"));
return m ? m[1].trim() : undefined;
}
/** Extract a YAML array field (tags) from a frontmatter block. */
function extractFrontmatterTags(fm: string): string[] {
const m = fm.match(/^tags:\s*\[([^\]]*)\]/m);
if (!m) return [];
return m[1]
.split(",")
.map((t) => t.trim().replace(/^"|"$/g, ""))
.filter(Boolean);
}
export function createRewriteDocTool(ctl: ToolsProviderController): Tool {
return tool({
name: "rewrite_doc",
description: `Replace the body of an existing note without changing its filename, title, or created date.
Use this tool when:
- A note already exists and you need to update its content (add a section, correct information, restructure)
- You want to make targeted edits without regenerating the entire document from scratch
- The user asks you to update, amend, or revise a specific note
Differences from memorize_doc:
- Takes a filename (not a title) — no rename risk
- Preserves the original filename, title, and created timestamp
- If tags are omitted, existing tags are kept unchanged
Parameters:
- filename: The exact filename of the note to update (e.g. "api-auth-notes.md"). Use find_doc to look it up.
- body: The new body content (plain Markdown, without frontmatter). Replaces the entire previous body.
- tags: Optional. If provided, replaces the existing tag list. If omitted, existing tags are preserved.
Returns:
- Confirmation with filename and a diff summary (characters before/after).
${formatToolMetaBlock()}`,
parameters: {
filename: z
.string()
.describe(
"The exact filename of the note to update (e.g. 'my-note.md'). Use the filename returned by find_doc."
),
body: z
.string()
.describe(
"The new body content (plain Markdown, without frontmatter). Replaces the entire previous body."
),
tags: z
.array(z.string())
.optional()
.describe(
"Optional. New tag list. If omitted, existing tags are kept as-is."
),
},
implementation: async (args) => {
const getter: any =
(ctl as any).getGlobalPluginConfig || (ctl as any).getGlobalConfig;
const gcfg = getter ? getter.call(ctl, globalConfigSchematics) : null;
const notesDirectory: string = gcfg?.get("notesDirectory") ?? "";
if (!notesDirectory) {
return "Error: Notes Directory is not configured. The initial plugin setup has not been completed yet — start a new chat and follow the setup guide, then set Notes Directory in the plugin global settings.";
}
const filePath = path.join(notesDirectory, args.filename);
let existingContent: string;
try {
existingContent = fs.readFileSync(filePath, "utf8");
} catch {
return `Error: File not found — "${args.filename}". Use find_doc to search available documents.`;
}
// Parse existing frontmatter
const fmMatch = existingContent.match(/^---\n([\s\S]*?)\n---\n/);
const fmBlock = fmMatch ? fmMatch[1] : "";
const existingTitle = extractFrontmatterField(fmBlock, "title") ?? args.filename.replace(/\.md$/, "");
const existingCreated = extractFrontmatterField(fmBlock, "created");
const existingTags = extractFrontmatterTags(fmBlock);
const tags = args.tags ?? existingTags;
const now = new Date().toISOString();
const frontmatter = buildFrontmatter(existingTitle, tags, now, existingCreated);
const newContent = frontmatter + args.body;
fs.writeFileSync(filePath, newContent, "utf8");
const charsBefore = existingContent.length;
const charsAfter = newContent.length;
const delta = charsAfter - charsBefore;
const sign = delta >= 0 ? "+" : "";
return `Updated "${args.filename}" — ${charsBefore} → ${charsAfter} chars (${sign}${delta}).`;
},
});
}