Project Files
src / tools / memorize.ts
/**
* memorize — write or overwrite a playbook document.
* Creates YAML frontmatter automatically; derives filename from title slug.
*/
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 type { IndexManager } from "../indexManager.js";
import { formatToolMetaBlock } from "../helpers/pluginMeta.js";
function slugify(title: string): string {
return title
.toLowerCase()
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "") // strip diacritics
.replace(/[^\w\s-]/g, "")
.trim()
.replace(/[\s_]+/g, "-")
.replace(/-+/g, "-")
.slice(0, 80);
}
function buildFrontmatter(title: string, tags: string[], now: string, existingCreated?: string): string {
const created = existingCreated ?? now;
const tagList = tags.map((t) => `"${t}"`).join(", ");
return `---\ntitle: "${title}"\ntags: [${tagList}]\ncreated: "${created}"\nupdated: "${now}"\n---\n\n`;
}
export function createMemorizeTool(
_ctl: ToolsProviderController,
index: IndexManager
): Tool {
return tool({
name: "memorize",
description: `Write a new document to the playbook, or completely replace an existing one.
The filename is derived automatically from the title (slugified).
YAML frontmatter (title, tags, created, updated) is generated and maintained automatically.
Use this tool when:
- The user explicitly asks you to remember something
- You derive or discover information useful for future conversations
- Summarising the outcome of a completed task
- Creating a structured reference note (how-tos, decisions, preferences)
Returns:
- Confirmation with title and filename
Examples:
- title: "API Auth Strategy", tags: ["api", "auth"], body: "Use OAuth2 for..."
- title: "User Preferences", tags: ["preferences"], body: "Prefers concise answers..."
${formatToolMetaBlock()}`,
parameters: {
title: z.string().describe("Document title (used as frontmatter title and to derive the filename)."),
tags: z
.array(z.string())
.default([])
.describe("List of tags to categorise this document."),
body: z
.string()
.describe("The main content of the document (plain Markdown, without frontmatter)."),
},
implementation: async (args) => {
const slug = slugify(args.title);
if (!slug) return "Error: Title must contain at least one word character.";
const filename = `${slug}.md`;
const filePath = path.join(index.getPlaybookDirectory(), filename);
const now = new Date().toISOString();
// Preserve original created date if file exists
let existingCreated: string | undefined;
if (fs.existsSync(filePath)) {
const existing = fs.readFileSync(filePath, "utf8");
const m = existing.match(/^created:\s*"?([^"\n]+)"?/m);
existingCreated = m ? m[1].trim() : undefined;
}
const frontmatter = buildFrontmatter(args.title, args.tags, now, existingCreated);
const content = `${frontmatter}${args.body}`;
await index.writeFile(filePath, content);
return `Saved "${args.title}" → ${filename}`;
},
});
}