shitton
This commit is contained in:
@@ -0,0 +1,303 @@
|
||||
import {AiTool} from "../tool-types";
|
||||
import path from "node:path";
|
||||
import {readdir, readFile, unlink, writeFile} from "node:fs/promises";
|
||||
import {notesDir, notesRootFile} from "../../index";
|
||||
import {asNonEmptyString} from "./utils";
|
||||
|
||||
export type NoteListItem = {
|
||||
fileName: string;
|
||||
filePath: string;
|
||||
relativePath: string;
|
||||
title: string;
|
||||
};
|
||||
|
||||
export type ListNotesResult =
|
||||
| { success: true; notes: NoteListItem[] }
|
||||
| { success: false; error: string };
|
||||
|
||||
export type GetNoteContentResult =
|
||||
| {
|
||||
success: true;
|
||||
fileName: string;
|
||||
filePath: string;
|
||||
relativePath: string;
|
||||
title: string;
|
||||
content: string;
|
||||
} | { success: false; error: string };
|
||||
|
||||
export const listNotesTool = {
|
||||
type: "function",
|
||||
function: {
|
||||
name: "list_notes",
|
||||
description: "Display all available Markdown notes from the notes directory.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {},
|
||||
required: [],
|
||||
},
|
||||
},
|
||||
} satisfies AiTool;
|
||||
|
||||
export const getNoteContentTool = {
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_note_content",
|
||||
description: "Get the full Markdown content of a specific note by its file name.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
fileName: {
|
||||
type: "string",
|
||||
description:
|
||||
"The file name of the note to read. It may be provided with or without the .md extension. Must not contain forbidden or unsafe characters such as /, \\, :, *, ?, \", <, >, |, or control characters.",
|
||||
},
|
||||
},
|
||||
required: ["fileName"],
|
||||
},
|
||||
},
|
||||
} satisfies AiTool;
|
||||
|
||||
export async function listNotes(): Promise<ListNotesResult> {
|
||||
console.log("LIST_NOTES");
|
||||
|
||||
try {
|
||||
const entries = await readdir(notesDir, {withFileTypes: true});
|
||||
|
||||
const markdownFiles = entries
|
||||
.filter((entry) => entry.isFile())
|
||||
.map((entry) => entry.name)
|
||||
.filter((fileName) => fileName.endsWith(".md"));
|
||||
|
||||
const notes: NoteListItem[] = await Promise.all(
|
||||
markdownFiles.map(async (fileName) => {
|
||||
const filePath = path.join(notesDir, fileName);
|
||||
const relativePath = path.relative(path.dirname(notesRootFile), filePath);
|
||||
|
||||
let content = "";
|
||||
try {
|
||||
content = await readFile(filePath, "utf-8");
|
||||
} catch {
|
||||
// Ignore content read errors for individual files.
|
||||
}
|
||||
|
||||
return {
|
||||
fileName,
|
||||
filePath,
|
||||
relativePath,
|
||||
title: extractNoteTitle(fileName, content),
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
notes.sort((a, b) => a.title.localeCompare(b.title));
|
||||
|
||||
return {success: true, notes};
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
return {success: false, error: `Failed to list notes: ${errorMessage}`};
|
||||
}
|
||||
}
|
||||
|
||||
export async function getNoteContent(
|
||||
args?: Record<string, unknown>,
|
||||
): Promise<GetNoteContentResult> {
|
||||
console.log("GET_NOTE_CONTENT; ARGS: ", args);
|
||||
|
||||
const fileName = asNonEmptyString(args?.fileName) ?? "";
|
||||
if (!fileName.trim().length) {
|
||||
return {success: false, error: "No file name provided"};
|
||||
}
|
||||
|
||||
const noteFilePath = buildSafeNoteFilePath(fileName);
|
||||
if (!noteFilePath) {
|
||||
return {success: false, error: "Invalid or unsafe file name provided"};
|
||||
}
|
||||
|
||||
try {
|
||||
const content = await readFile(noteFilePath, "utf-8");
|
||||
const normalizedFileName = path.basename(noteFilePath);
|
||||
const relativePath = path.relative(path.dirname(notesRootFile), noteFilePath);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
fileName: normalizedFileName,
|
||||
filePath: noteFilePath,
|
||||
relativePath,
|
||||
title: extractNoteTitle(normalizedFileName, content),
|
||||
content,
|
||||
};
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
return {success: false, error: `Failed to read note: ${errorMessage}`};
|
||||
}
|
||||
}
|
||||
|
||||
function extractNoteTitle(fileName: string, content: string): string {
|
||||
const headingMatch = content.match(/^#\s+(.+)$/m);
|
||||
const heading = headingMatch?.[1]?.trim();
|
||||
|
||||
if (heading) {
|
||||
return heading;
|
||||
}
|
||||
|
||||
return path.basename(fileName, ".md");
|
||||
}
|
||||
|
||||
export function buildSafeNoteFilePath(fileName: string): string | null {
|
||||
const normalizedFileName = fileName.endsWith(".md") ? fileName : `${fileName}.md`;
|
||||
|
||||
if (!normalizedFileName.trim().length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const unsafeFileNamePattern = /[/\\:*?"<>|\x00-\x1F]/;
|
||||
if (unsafeFileNamePattern.test(normalizedFileName)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const resolvedNotesDir = path.resolve(notesDir);
|
||||
const resolvedFilePath = path.resolve(notesDir, normalizedFileName);
|
||||
|
||||
if (!resolvedFilePath.startsWith(resolvedNotesDir + path.sep)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return resolvedFilePath;
|
||||
}
|
||||
|
||||
export type UpdateNoteContentResult =
|
||||
| { success: true; filePath: string }
|
||||
| { success: false; error: string };
|
||||
|
||||
export type DeleteNoteResult =
|
||||
| { success: true; filePath: string }
|
||||
| { success: false; error: string };
|
||||
|
||||
export const updateNoteContentTool = {
|
||||
type: "function",
|
||||
function: {
|
||||
name: "update_note_content",
|
||||
description: "Update the full Markdown content of an existing note by its file name.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
fileName: {
|
||||
type: "string",
|
||||
description:
|
||||
"The file name of the note to update. It may be provided with or without the .md extension. Must not contain forbidden or unsafe characters such as /, \\, :, *, ?, \", <, >, |, or control characters.",
|
||||
},
|
||||
content: {
|
||||
type: "string",
|
||||
description:
|
||||
"The new full content of the note formatted as valid Markdown. This replaces the previous content completely.",
|
||||
},
|
||||
},
|
||||
required: ["fileName", "content"],
|
||||
},
|
||||
},
|
||||
} satisfies AiTool;
|
||||
|
||||
export const deleteNoteTool = {
|
||||
type: "function",
|
||||
function: {
|
||||
name: "delete_note",
|
||||
description: "Delete an existing Markdown note by its file name and remove its link from the notes root file if present.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
fileName: {
|
||||
type: "string",
|
||||
description:
|
||||
"The file name of the note to delete. It may be provided with or without the .md extension. Must not contain forbidden or unsafe characters such as /, \\, :, *, ?, \", <, >, |, or control characters.",
|
||||
},
|
||||
},
|
||||
required: ["fileName"],
|
||||
},
|
||||
},
|
||||
} satisfies AiTool;
|
||||
|
||||
export async function updateNoteContent(
|
||||
args?: Record<string, unknown>,
|
||||
): Promise<UpdateNoteContentResult> {
|
||||
console.log("UPDATE_NOTE_CONTENT; ARGS: ", args);
|
||||
|
||||
const fileName = asNonEmptyString(args?.fileName) ?? "";
|
||||
if (!fileName.trim().length) {
|
||||
return {success: false, error: "No file name provided"};
|
||||
}
|
||||
|
||||
const content = asNonEmptyString(args?.content) ?? "";
|
||||
if (!content.trim().length) {
|
||||
return {success: false, error: "No content provided"};
|
||||
}
|
||||
|
||||
const noteFilePath = buildSafeNoteFilePath(fileName);
|
||||
if (!noteFilePath) {
|
||||
return {success: false, error: "Invalid or unsafe file name provided"};
|
||||
}
|
||||
|
||||
try {
|
||||
await readFile(noteFilePath, "utf-8");
|
||||
await writeFile(noteFilePath, content, "utf-8");
|
||||
|
||||
return {success: true, filePath: noteFilePath};
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
return {success: false, error: `Failed to update note: ${errorMessage}`};
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteNote(
|
||||
args?: Record<string, unknown>,
|
||||
): Promise<DeleteNoteResult> {
|
||||
console.log("DELETE_NOTE; ARGS: ", args);
|
||||
|
||||
const fileName = asNonEmptyString(args?.fileName) ?? "";
|
||||
if (!fileName.trim().length) {
|
||||
return {success: false, error: "No file name provided"};
|
||||
}
|
||||
|
||||
const noteFilePath = buildSafeNoteFilePath(fileName);
|
||||
if (!noteFilePath) {
|
||||
return {success: false, error: "Invalid or unsafe file name provided"};
|
||||
}
|
||||
|
||||
try {
|
||||
await unlink(noteFilePath);
|
||||
await removeNoteLinkFromRoot(noteFilePath);
|
||||
|
||||
return {success: true, filePath: noteFilePath};
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
return {success: false, error: `Failed to delete note: ${errorMessage}`};
|
||||
}
|
||||
}
|
||||
|
||||
async function removeNoteLinkFromRoot(noteFilePath: string): Promise<void> {
|
||||
let rootContent: string;
|
||||
|
||||
try {
|
||||
rootContent = await readFile(notesRootFile, "utf-8");
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
const relativePath = path.relative(path.dirname(notesRootFile), noteFilePath);
|
||||
const normalizedRelativePath = relativePath.replaceAll("\\", "\\\\");
|
||||
|
||||
const escapedRelativePath = escapeRegExp(normalizedRelativePath);
|
||||
const linkLinePattern = new RegExp(
|
||||
`^\\s*[-*]\\s+\\[[^\\]]+]\\(${escapedRelativePath}\\)\\s*$\\n?`,
|
||||
"gm",
|
||||
);
|
||||
|
||||
const updatedRootContent = rootContent.replace(linkLinePattern, "");
|
||||
|
||||
if (updatedRootContent !== rootContent) {
|
||||
await writeFile(notesRootFile, updatedRootContent.trimEnd() + "\n", "utf-8");
|
||||
}
|
||||
}
|
||||
|
||||
function escapeRegExp(value: string): string {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
Reference in New Issue
Block a user