src / tools / notesTool.ts
src / tools / notesTool.ts
import { tool, type Tool } from "@lmstudio/sdk";
import { z } from "zod";
import { AgenticError } from "../core/errors";
import { errorResult, okResult } from "../core/result";
import { validateNoteName } from "../notes/noteStore";
import type { PluginRuntime } from "../runtime";
type NotesAction = "write" | "append" | "read" | "list" | "delete";
/** `name: "MEMORY"` / `"memory.md"` (any case) means the workspace memory file, exactly like `memory: true`. */
const MEMORY_ALIAS = /^memory(\.md)?$/i;
function targetsMemory(input: { name?: string; memory?: boolean }): boolean {
return input.memory === true || MEMORY_ALIAS.test((input.name ?? "").trim());
}
/**
* Scratchpad notes and workspace memory. Deliberately ungated: notes are
* plugin-owned state under `.agentic/`, never workspace edits, so every
* permission mode may use them freely.
*/
export function createNotesTool(runtime: PluginRuntime): Tool {
return tool({
name: "workspace_notes",
description:
"Scratchpad notes and memory in .agentic/notes/ and .agentic/MEMORY.md. Actions: write, append, read, list, delete. memory=true targets MEMORY.md, durable workspace facts (conventions, decisions, gotchas): read it at the start of every task and append what a future session must know. Keep working notes here, not in chat: they survive compression. No approval needed.",
parameters: {
action: z.enum(["write", "append", "read", "list", "delete"]),
name: z.string().optional(),
content: z.string().optional(),
memory: z.boolean().optional(),
limit: z.number().int().min(1).max(200).optional(),
},
implementation: async (input: {
action: NotesAction;
name?: string;
content?: string;
memory?: boolean;
limit?: number;
}) => {
try {
const notes = runtime.notes;
if (input.action === "list") {
const [list, memory] = await Promise.all([notes.list(), notes.readMemory()]);
const limited = list.slice(0, input.limit ?? 100);
const shown = limited.length < list.length ? ` (showing ${limited.length})` : "";
return okResult({
operation: "notes.list",
summary: `${list.length} note(s)${shown}; MEMORY.md ${memory.exists ? `present (${memory.bytes} bytes)` : "absent"}.`,
data: {
notes: limited,
total: list.length,
memory: { exists: memory.exists, path: memory.path, bytes: memory.bytes },
},
facts: [
`workspace memory ${memory.exists ? "present" : "absent"} at ${memory.path}`,
...limited.slice(0, 20).map((note) => `note ${note.name} at ${note.path}`),
],
omit: ["data.notes"],
});
}
if (targetsMemory(input)) {
if (input.action === "read") {
const memory = await notes.readMemory();
return okResult({
operation: "notes.read",
summary: memory.exists
? `Read MEMORY.md (${memory.bytes} bytes).`
: "MEMORY.md does not exist yet.",
data: {
name: "MEMORY",
path: memory.path,
content: memory.content,
bytes: memory.bytes,
exists: memory.exists,
},
importance: "high",
facts: [`workspace memory at ${memory.path}`],
omit: ["data.content"],
});
}
if (input.action === "delete") {
throw new AgenticError(
"INVALID_INPUT",
"MEMORY.md cannot be deleted; overwrite it with write instead.",
);
}
if (input.content === undefined) {
throw new AgenticError("INVALID_INPUT", `workspace_notes ${input.action} requires content.`);
}
const summary =
input.action === "write"
? await notes.writeMemory(input.content)
: await notes.appendMemory(input.content);
return okResult({
operation: `notes.${input.action}`,
summary: `MEMORY.md ${input.action === "write" ? "written" : "appended"} (${summary.bytes} bytes).`,
data: summary,
importance: "high",
facts: [`workspace memory updated at ${summary.path}`],
});
}
if (!input.name) {
throw new AgenticError(
"INVALID_INPUT",
`workspace_notes ${input.action} requires name (or memory=true).`,
);
}
if (input.action === "read") {
const note = await notes.read(input.name);
return okResult({
operation: "notes.read",
summary: `Read note ${note.name} (${note.bytes} bytes).`,
data: note,
importance: "high",
facts: [`note ${note.name} at ${note.path} (${note.bytes} bytes)`],
omit: ["data.content"],
});
}
if (input.action === "delete") {
const name = validateNoteName(input.name);
const path = notes.notePath(name);
await notes.remove(name);
return okResult({
operation: "notes.delete",
summary: `Deleted note ${name}.`,
data: { name, path },
facts: [`note ${name} deleted from ${path}`],
});
}
if (input.content === undefined) {
throw new AgenticError("INVALID_INPUT", `workspace_notes ${input.action} requires content.`);
}
const summary =
input.action === "write"
? await notes.write(input.name, input.content)
: await notes.append(input.name, input.content);
return okResult({
operation: `notes.${input.action}`,
summary: `Note ${summary.name} ${input.action === "write" ? "written" : "appended"} (${summary.bytes} bytes).`,
data: summary,
importance: "high",
facts: [`note ${summary.name} updated at ${summary.path}`],
});
} catch (error) {
return errorResult(`notes.${input.action}`, error);
}
},
});
}
import { tool, type Tool } from "@lmstudio/sdk";
import { z } from "zod";
import { AgenticError } from "../core/errors";
import { errorResult, okResult } from "../core/result";
import { validateNoteName } from "../notes/noteStore";
import type { PluginRuntime } from "../runtime";
type NotesAction = "write" | "append" | "read" | "list" | "delete";
/** `name: "MEMORY"` / `"memory.md"` (any case) means the workspace memory file, exactly like `memory: true`. */
const MEMORY_ALIAS = /^memory(\.md)?$/i;
function targetsMemory(input: { name?: string; memory?: boolean }): boolean {
return input.memory === true || MEMORY_ALIAS.test((input.name ?? "").trim());
}
/**
* Scratchpad notes and workspace memory. Deliberately ungated: notes are
* plugin-owned state under `.agentic/`, never workspace edits, so every
* permission mode may use them freely.
*/
export function createNotesTool(runtime: PluginRuntime): Tool {
return tool({
name: "workspace_notes",
description:
"Scratchpad notes and memory in .agentic/notes/ and .agentic/MEMORY.md. Actions: write, append, read, list, delete. memory=true targets MEMORY.md, durable workspace facts (conventions, decisions, gotchas): read it at the start of every task and append what a future session must know. Keep working notes here, not in chat: they survive compression. No approval needed.",
parameters: {
action: z.enum(["write", "append", "read", "list", "delete"]),
name: z.string().optional(),
content: z.string().optional(),
memory: z.boolean().optional(),
limit: z.number().int().min(1).max(200).optional(),
},
implementation: async (input: {
action: NotesAction;
name?: string;
content?: string;
memory?: boolean;
limit?: number;
}) => {
try {
const notes = runtime.notes;
if (input.action === "list") {
const [list, memory] = await Promise.all([notes.list(), notes.readMemory()]);
const limited = list.slice(0, input.limit ?? 100);
const shown = limited.length < list.length ? ` (showing ${limited.length})` : "";
return okResult({
operation: "notes.list",
summary: `${list.length} note(s)${shown}; MEMORY.md ${memory.exists ? `present (${memory.bytes} bytes)` : "absent"}.`,
data: {
notes: limited,
total: list.length,
memory: { exists: memory.exists, path: memory.path, bytes: memory.bytes },
},
facts: [
`workspace memory ${memory.exists ? "present" : "absent"} at ${memory.path}`,
...limited.slice(0, 20).map((note) => `note ${note.name} at ${note.path}`),
],
omit: ["data.notes"],
});
}
if (targetsMemory(input)) {
if (input.action === "read") {
const memory = await notes.readMemory();
return okResult({
operation: "notes.read",
summary: memory.exists
? `Read MEMORY.md (${memory.bytes} bytes).`
: "MEMORY.md does not exist yet.",
data: {
name: "MEMORY",
path: memory.path,
content: memory.content,
bytes: memory.bytes,
exists: memory.exists,
},
importance: "high",
facts: [`workspace memory at ${memory.path}`],
omit: ["data.content"],
});
}
if (input.action === "delete") {
throw new AgenticError(
"INVALID_INPUT",
"MEMORY.md cannot be deleted; overwrite it with write instead.",
);
}
if (input.content === undefined) {
throw new AgenticError("INVALID_INPUT", `workspace_notes ${input.action} requires content.`);
}
const summary =
input.action === "write"
? await notes.writeMemory(input.content)
: await notes.appendMemory(input.content);
return okResult({
operation: `notes.${input.action}`,
summary: `MEMORY.md ${input.action === "write" ? "written" : "appended"} (${summary.bytes} bytes).`,
data: summary,
importance: "high",
facts: [`workspace memory updated at ${summary.path}`],
});
}
if (!input.name) {
throw new AgenticError(
"INVALID_INPUT",
`workspace_notes ${input.action} requires name (or memory=true).`,
);
}
if (input.action === "read") {
const note = await notes.read(input.name);
return okResult({
operation: "notes.read",
summary: `Read note ${note.name} (${note.bytes} bytes).`,
data: note,
importance: "high",
facts: [`note ${note.name} at ${note.path} (${note.bytes} bytes)`],
omit: ["data.content"],
});
}
if (input.action === "delete") {
const name = validateNoteName(input.name);
const path = notes.notePath(name);
await notes.remove(name);
return okResult({
operation: "notes.delete",
summary: `Deleted note ${name}.`,
data: { name, path },
facts: [`note ${name} deleted from ${path}`],
});
}
if (input.content === undefined) {
throw new AgenticError("INVALID_INPUT", `workspace_notes ${input.action} requires content.`);
}
const summary =
input.action === "write"
? await notes.write(input.name, input.content)
: await notes.append(input.name, input.content);
return okResult({
operation: `notes.${input.action}`,
summary: `Note ${summary.name} ${input.action === "write" ? "written" : "appended"} (${summary.bytes} bytes).`,
data: summary,
importance: "high",
facts: [`note ${summary.name} updated at ${summary.path}`],
});
} catch (error) {
return errorResult(`notes.${input.action}`, error);
}
},
});
}