joplin-mcp-server 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +384 -0
- package/dist/bin/cli.d.ts +2 -0
- package/dist/bin/cli.js +7 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +204 -0
- package/dist/lib/joplin-api-client.d.ts +23 -0
- package/dist/lib/joplin-api-client.js +110 -0
- package/dist/lib/logger.d.ts +21 -0
- package/dist/lib/logger.js +68 -0
- package/dist/lib/parse-args.d.ts +2 -0
- package/dist/lib/parse-args.js +81 -0
- package/dist/lib/tools/base-tool.d.ts +27 -0
- package/dist/lib/tools/base-tool.js +24 -0
- package/dist/lib/tools/create-folder.d.ts +9 -0
- package/dist/lib/tools/create-folder.js +79 -0
- package/dist/lib/tools/create-note.d.ts +13 -0
- package/dist/lib/tools/create-note.js +88 -0
- package/dist/lib/tools/delete-folder.d.ts +10 -0
- package/dist/lib/tools/delete-folder.js +138 -0
- package/dist/lib/tools/delete-note.d.ts +9 -0
- package/dist/lib/tools/delete-note.js +92 -0
- package/dist/lib/tools/edit-folder.d.ts +10 -0
- package/dist/lib/tools/edit-folder.js +136 -0
- package/dist/lib/tools/edit-note.d.ts +15 -0
- package/dist/lib/tools/edit-note.js +153 -0
- package/dist/lib/tools/index.d.ts +12 -0
- package/dist/lib/tools/index.js +12 -0
- package/dist/lib/tools/list-notebooks.d.ts +7 -0
- package/dist/lib/tools/list-notebooks.js +59 -0
- package/dist/lib/tools/read-multi-note.d.ts +5 -0
- package/dist/lib/tools/read-multi-note.js +108 -0
- package/dist/lib/tools/read-note.d.ts +5 -0
- package/dist/lib/tools/read-note.js +80 -0
- package/dist/lib/tools/read-notebook.d.ts +5 -0
- package/dist/lib/tools/read-notebook.js +66 -0
- package/dist/lib/tools/search-notes.d.ts +5 -0
- package/dist/lib/tools/search-notes.js +68 -0
- package/dist/tests/integration/joplin-integration.test.d.ts +1 -0
- package/dist/tests/integration/joplin-integration.test.js +117 -0
- package/dist/tests/manual/create-folder.test.d.ts +1 -0
- package/dist/tests/manual/create-folder.test.js +81 -0
- package/dist/tests/manual/create-note.test.d.ts +1 -0
- package/dist/tests/manual/create-note.test.js +84 -0
- package/dist/tests/manual/delete-folder.test.d.ts +1 -0
- package/dist/tests/manual/delete-folder.test.js +118 -0
- package/dist/tests/manual/delete-note.test.d.ts +1 -0
- package/dist/tests/manual/delete-note.test.js +101 -0
- package/dist/tests/manual/edit-folder.test.d.ts +1 -0
- package/dist/tests/manual/edit-folder.test.js +104 -0
- package/dist/tests/manual/edit-note.test.d.ts +1 -0
- package/dist/tests/manual/edit-note.test.js +118 -0
- package/dist/tests/manual/list-notebooks.test.d.ts +1 -0
- package/dist/tests/manual/list-notebooks.test.js +42 -0
- package/dist/tests/manual/read-note.test.d.ts +1 -0
- package/dist/tests/manual/read-note.test.js +54 -0
- package/dist/tests/manual/search-notes.test.d.ts +1 -0
- package/dist/tests/manual/search-notes.test.js +43 -0
- package/dist/tests/unit/create-tools.test.d.ts +1 -0
- package/dist/tests/unit/create-tools.test.js +223 -0
- package/dist/tests/unit/delete-tools.test.d.ts +1 -0
- package/dist/tests/unit/delete-tools.test.js +225 -0
- package/dist/tests/unit/edit-tools.test.d.ts +1 -0
- package/dist/tests/unit/edit-tools.test.js +261 -0
- package/dist/tests/unit/joplin-api-client.test.d.ts +1 -0
- package/dist/tests/unit/joplin-api-client.test.js +154 -0
- package/dist/vitest.config.d.ts +2 -0
- package/dist/vitest.config.js +22 -0
- package/dist/vitest.setup.d.ts +1 -0
- package/dist/vitest.setup.js +24 -0
- package/package.json +58 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import BaseTool from "./base-tool.js";
|
|
2
|
+
class DeleteFolder extends BaseTool {
|
|
3
|
+
async call(options) {
|
|
4
|
+
if (!options || typeof options !== "object") {
|
|
5
|
+
return 'Please provide folder deletion options. Example: delete_folder {"folder_id": "abc123", "confirm": true}';
|
|
6
|
+
}
|
|
7
|
+
// Validate required folder_id
|
|
8
|
+
if (!options.folder_id) {
|
|
9
|
+
return 'Please provide folder deletion options. Example: delete_folder {"folder_id": "abc123", "confirm": true}';
|
|
10
|
+
}
|
|
11
|
+
const folderIdError = this.validateId(options.folder_id, "notebook");
|
|
12
|
+
if (folderIdError) {
|
|
13
|
+
return folderIdError.replace("notebook ID", "folder ID").replace("notebook_id", "folder_id");
|
|
14
|
+
}
|
|
15
|
+
// Require explicit confirmation for safety
|
|
16
|
+
if (!options.confirm) {
|
|
17
|
+
return `⚠️ This will permanently delete the notebook/folder!\n\nTo confirm deletion, use:\ndelete_folder {"folder_id": "${options.folder_id}", "confirm": true}\n\n⚠️ This action cannot be undone!`;
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
// First, get the folder details to show what's being deleted
|
|
21
|
+
const folderToDelete = await this.apiClient.get(`/folders/${options.folder_id}`, {
|
|
22
|
+
query: { fields: "id,title,parent_id" },
|
|
23
|
+
});
|
|
24
|
+
if (!folderToDelete || !folderToDelete.id) {
|
|
25
|
+
return `Folder with ID "${options.folder_id}" not found.\n\nUse list_notebooks to see available folders and their IDs.`;
|
|
26
|
+
}
|
|
27
|
+
// Check if folder contains notes or subfolders
|
|
28
|
+
const [notes, subfolders] = await Promise.all([
|
|
29
|
+
this.apiClient
|
|
30
|
+
.get(`/folders/${options.folder_id}/notes`, {
|
|
31
|
+
query: { fields: "id,title" },
|
|
32
|
+
})
|
|
33
|
+
.catch(() => ({ items: [] })),
|
|
34
|
+
this.apiClient
|
|
35
|
+
.get("/folders", {
|
|
36
|
+
query: { fields: "id,title,parent_id" },
|
|
37
|
+
})
|
|
38
|
+
.then((response) => ({
|
|
39
|
+
items: response.items?.filter((folder) => folder.parent_id === options.folder_id) || [],
|
|
40
|
+
}))
|
|
41
|
+
.catch(() => ({ items: [] })),
|
|
42
|
+
]);
|
|
43
|
+
const noteCount = notes.items?.length || 0;
|
|
44
|
+
const subfolderCount = subfolders.items?.length || 0;
|
|
45
|
+
const totalContent = noteCount + subfolderCount;
|
|
46
|
+
// Warn if folder is not empty and force is not specified
|
|
47
|
+
if (totalContent > 0 && !options.force) {
|
|
48
|
+
const resultLines = [];
|
|
49
|
+
resultLines.push(`⚠️ Cannot delete non-empty notebook!`);
|
|
50
|
+
resultLines.push("");
|
|
51
|
+
resultLines.push(`📁 Notebook: "${folderToDelete.title}"`);
|
|
52
|
+
resultLines.push(` Contains: ${noteCount} notes and ${subfolderCount} subfolders`);
|
|
53
|
+
if (noteCount > 0) {
|
|
54
|
+
resultLines.push("");
|
|
55
|
+
resultLines.push(`📝 Contains ${noteCount} notes:`);
|
|
56
|
+
notes.items.slice(0, 5).forEach((note) => {
|
|
57
|
+
resultLines.push(` - ${note.title || "Untitled"}`);
|
|
58
|
+
});
|
|
59
|
+
if (noteCount > 5) {
|
|
60
|
+
resultLines.push(` ... and ${noteCount - 5} more notes`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (subfolderCount > 0) {
|
|
64
|
+
resultLines.push("");
|
|
65
|
+
resultLines.push(`📁 Contains ${subfolderCount} subfolders:`);
|
|
66
|
+
subfolders.items.slice(0, 5).forEach((folder) => {
|
|
67
|
+
resultLines.push(` - ${folder.title}`);
|
|
68
|
+
});
|
|
69
|
+
if (subfolderCount > 5) {
|
|
70
|
+
resultLines.push(` ... and ${subfolderCount - 5} more folders`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
resultLines.push("");
|
|
74
|
+
resultLines.push(`💡 Options:`);
|
|
75
|
+
resultLines.push(` 1. Move or delete the contents first, then delete the folder`);
|
|
76
|
+
resultLines.push(` 2. Force delete (⚠️ DESTROYS ALL CONTENT):`);
|
|
77
|
+
resultLines.push(` delete_folder {"folder_id": "${options.folder_id}", "confirm": true, "force": true}`);
|
|
78
|
+
resultLines.push("");
|
|
79
|
+
resultLines.push(`⚠️ Force delete will permanently delete ALL ${totalContent} items inside!`);
|
|
80
|
+
return resultLines.join("\n");
|
|
81
|
+
}
|
|
82
|
+
// Get parent folder info if available
|
|
83
|
+
let parentInfo = "Top level";
|
|
84
|
+
if (folderToDelete.parent_id) {
|
|
85
|
+
try {
|
|
86
|
+
const parentFolder = await this.apiClient.get(`/folders/${folderToDelete.parent_id}`, {
|
|
87
|
+
query: { fields: "title" },
|
|
88
|
+
});
|
|
89
|
+
if (parentFolder?.title) {
|
|
90
|
+
parentInfo = `Inside "${parentFolder.title}" (notebook_id: "${folderToDelete.parent_id}")`;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
parentInfo = `Parent ID: ${folderToDelete.parent_id}`;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// Delete the folder
|
|
98
|
+
await this.apiClient.delete(`/folders/${options.folder_id}`);
|
|
99
|
+
// Format success response
|
|
100
|
+
const resultLines = [];
|
|
101
|
+
resultLines.push(`🗑️ Successfully deleted notebook!`);
|
|
102
|
+
resultLines.push("");
|
|
103
|
+
resultLines.push(`📁 Deleted Notebook Details:`);
|
|
104
|
+
resultLines.push(` Title: "${folderToDelete.title}"`);
|
|
105
|
+
resultLines.push(` Folder ID: ${folderToDelete.id}`);
|
|
106
|
+
resultLines.push(` Location: ${parentInfo}`);
|
|
107
|
+
if (totalContent > 0) {
|
|
108
|
+
resultLines.push(` Deleted Content: ${noteCount} notes and ${subfolderCount} subfolders`);
|
|
109
|
+
resultLines.push("");
|
|
110
|
+
resultLines.push(`⚠️ All ${totalContent} items inside have been permanently deleted!`);
|
|
111
|
+
}
|
|
112
|
+
resultLines.push("");
|
|
113
|
+
resultLines.push(`⚠️ This notebook has been permanently deleted and cannot be recovered.`);
|
|
114
|
+
if (folderToDelete.parent_id) {
|
|
115
|
+
resultLines.push("");
|
|
116
|
+
resultLines.push(`🔗 Related actions:`);
|
|
117
|
+
resultLines.push(` - View parent notebook: read_notebook notebook_id="${folderToDelete.parent_id}"`);
|
|
118
|
+
resultLines.push(` - View all notebooks: list_notebooks`);
|
|
119
|
+
}
|
|
120
|
+
return resultLines.join("\n");
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
if (error.response) {
|
|
124
|
+
if (error.response.status === 404) {
|
|
125
|
+
return `Folder with ID "${options.folder_id}" not found.\n\nUse list_notebooks to see available folders and their IDs.`;
|
|
126
|
+
}
|
|
127
|
+
if (error.response.status === 403) {
|
|
128
|
+
return `Permission denied: Cannot delete folder with ID "${options.folder_id}".\n\nThis might be a protected system folder.`;
|
|
129
|
+
}
|
|
130
|
+
if (error.response.status === 409) {
|
|
131
|
+
return `Cannot delete folder: It may contain items that prevent deletion.\n\nTry moving or deleting the contents first, or use force option.`;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return this.formatError(error, "deleting folder");
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
export default DeleteFolder;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import BaseTool from "./base-tool.js";
|
|
2
|
+
class DeleteNote extends BaseTool {
|
|
3
|
+
async call(options) {
|
|
4
|
+
if (!options || typeof options !== "object") {
|
|
5
|
+
return 'Please provide note deletion options. Example: delete_note {"note_id": "abc123", "confirm": true}';
|
|
6
|
+
}
|
|
7
|
+
// Validate required note_id
|
|
8
|
+
if (!options.note_id) {
|
|
9
|
+
return 'Please provide note deletion options. Example: delete_note {"note_id": "abc123", "confirm": true}';
|
|
10
|
+
}
|
|
11
|
+
const noteIdError = this.validateId(options.note_id, "note");
|
|
12
|
+
if (noteIdError) {
|
|
13
|
+
return noteIdError;
|
|
14
|
+
}
|
|
15
|
+
// Require explicit confirmation for safety
|
|
16
|
+
if (!options.confirm) {
|
|
17
|
+
return `⚠️ This will permanently delete the note!\n\nTo confirm deletion, use:\ndelete_note {"note_id": "${options.note_id}", "confirm": true}\n\n⚠️ This action cannot be undone!`;
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
// First, get the note details to show what's being deleted
|
|
21
|
+
const noteToDelete = await this.apiClient.get(`/notes/${options.note_id}`, {
|
|
22
|
+
query: { fields: "id,title,body,parent_id,is_todo,todo_completed,created_time,updated_time" },
|
|
23
|
+
});
|
|
24
|
+
if (!noteToDelete || !noteToDelete.id) {
|
|
25
|
+
return `Note with ID "${options.note_id}" not found.\n\nUse search_notes to find notes and their IDs.`;
|
|
26
|
+
}
|
|
27
|
+
// Get notebook info if available
|
|
28
|
+
let notebookInfo = "Root level";
|
|
29
|
+
if (noteToDelete.parent_id) {
|
|
30
|
+
try {
|
|
31
|
+
const notebook = await this.apiClient.get(`/folders/${noteToDelete.parent_id}`, {
|
|
32
|
+
query: { fields: "title" },
|
|
33
|
+
});
|
|
34
|
+
if (notebook?.title) {
|
|
35
|
+
notebookInfo = `"${notebook.title}" (notebook_id: "${noteToDelete.parent_id}")`;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
notebookInfo = `Notebook ID: ${noteToDelete.parent_id}`;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// Delete the note
|
|
43
|
+
await this.apiClient.delete(`/notes/${options.note_id}`);
|
|
44
|
+
// Format success response
|
|
45
|
+
const resultLines = [];
|
|
46
|
+
resultLines.push(`🗑️ Successfully deleted note!`);
|
|
47
|
+
resultLines.push("");
|
|
48
|
+
resultLines.push(`📝 Deleted Note Details:`);
|
|
49
|
+
resultLines.push(` Title: "${noteToDelete.title || "Untitled"}"`);
|
|
50
|
+
resultLines.push(` Note ID: ${noteToDelete.id}`);
|
|
51
|
+
resultLines.push(` Location: ${notebookInfo}`);
|
|
52
|
+
if (noteToDelete.is_todo) {
|
|
53
|
+
const status = noteToDelete.todo_completed ? "Completed" : "Not completed";
|
|
54
|
+
resultLines.push(` Type: Todo (${status})`);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
resultLines.push(` Type: Regular note`);
|
|
58
|
+
}
|
|
59
|
+
const createdDate = this.formatDate(noteToDelete.created_time);
|
|
60
|
+
const updatedDate = this.formatDate(noteToDelete.updated_time);
|
|
61
|
+
resultLines.push(` Created: ${createdDate}`);
|
|
62
|
+
resultLines.push(` Last Updated: ${updatedDate}`);
|
|
63
|
+
// Show content preview if available
|
|
64
|
+
if (noteToDelete.body) {
|
|
65
|
+
const preview = noteToDelete.body.substring(0, 100).replace(/\n/g, " ");
|
|
66
|
+
const truncated = noteToDelete.body.length > 100 ? "..." : "";
|
|
67
|
+
resultLines.push(` Content Preview: ${preview}${truncated}`);
|
|
68
|
+
}
|
|
69
|
+
resultLines.push("");
|
|
70
|
+
resultLines.push(`⚠️ This note has been permanently deleted and cannot be recovered.`);
|
|
71
|
+
if (noteToDelete.parent_id) {
|
|
72
|
+
resultLines.push("");
|
|
73
|
+
resultLines.push(`🔗 Related actions:`);
|
|
74
|
+
resultLines.push(` - View containing notebook: read_notebook notebook_id="${noteToDelete.parent_id}"`);
|
|
75
|
+
resultLines.push(` - Search for similar notes: search_notes query="${noteToDelete.title}"`);
|
|
76
|
+
}
|
|
77
|
+
return resultLines.join("\n");
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
if (error.response) {
|
|
81
|
+
if (error.response.status === 404) {
|
|
82
|
+
return `Note with ID "${options.note_id}" not found.\n\nUse search_notes to find notes and their IDs.`;
|
|
83
|
+
}
|
|
84
|
+
if (error.response.status === 403) {
|
|
85
|
+
return `Permission denied: Cannot delete note with ID "${options.note_id}".\n\nThis might be a protected system note.`;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return this.formatError(error, "deleting note");
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
export default DeleteNote;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import BaseTool from "./base-tool.js";
|
|
2
|
+
interface EditFolderOptions {
|
|
3
|
+
folder_id: string;
|
|
4
|
+
title?: string | undefined;
|
|
5
|
+
parent_id?: string | undefined;
|
|
6
|
+
}
|
|
7
|
+
declare class EditFolder extends BaseTool {
|
|
8
|
+
call(options: EditFolderOptions): Promise<string>;
|
|
9
|
+
}
|
|
10
|
+
export default EditFolder;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import BaseTool from "./base-tool.js";
|
|
2
|
+
class EditFolder extends BaseTool {
|
|
3
|
+
async call(options) {
|
|
4
|
+
if (!options || typeof options !== "object") {
|
|
5
|
+
return 'Please provide folder edit options. Example: edit_folder {"folder_id": "abc123", "title": "New Name"}';
|
|
6
|
+
}
|
|
7
|
+
// Validate required folder_id
|
|
8
|
+
if (!options.folder_id) {
|
|
9
|
+
return 'Please provide folder edit options. Example: edit_folder {"folder_id": "abc123", "title": "New Name"}';
|
|
10
|
+
}
|
|
11
|
+
const folderIdError = this.validateId(options.folder_id, "notebook");
|
|
12
|
+
if (folderIdError) {
|
|
13
|
+
return folderIdError.replace("notebook ID", "folder ID").replace("notebook_id", "folder_id");
|
|
14
|
+
}
|
|
15
|
+
// Validate that we have at least one field to update
|
|
16
|
+
const updateFields = ["title", "parent_id"];
|
|
17
|
+
const hasUpdate = updateFields.some((field) => options[field] !== undefined);
|
|
18
|
+
if (!hasUpdate) {
|
|
19
|
+
return "Please provide at least one field to update. Available fields: title, parent_id";
|
|
20
|
+
}
|
|
21
|
+
// Validate title if provided
|
|
22
|
+
if (options.title !== undefined && (typeof options.title !== "string" || options.title.trim() === "")) {
|
|
23
|
+
return "Title must be a non-empty string.";
|
|
24
|
+
}
|
|
25
|
+
// Validate parent_id if provided
|
|
26
|
+
if (options.parent_id !== undefined && options.parent_id !== null && options.parent_id !== "") {
|
|
27
|
+
if (options.parent_id.length < 10 || !options.parent_id.match(/[a-f0-9]/i)) {
|
|
28
|
+
return `Error: "${options.parent_id}" does not appear to be a valid parent notebook ID.\n\nNotebook IDs are long alphanumeric strings like "58a0a29f68bc4141b49c99f5d367638a".\n\nUse list_notebooks to see available notebooks and their IDs.`;
|
|
29
|
+
}
|
|
30
|
+
// Prevent self-parenting
|
|
31
|
+
if (options.parent_id === options.folder_id) {
|
|
32
|
+
return "Error: A folder cannot be its own parent.";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
// First, get the current folder to show before/after comparison
|
|
37
|
+
const currentFolder = await this.apiClient.get(`/folders/${options.folder_id}`, {
|
|
38
|
+
query: { fields: "id,title,parent_id" },
|
|
39
|
+
});
|
|
40
|
+
if (!currentFolder || !currentFolder.id) {
|
|
41
|
+
return `Folder with ID "${options.folder_id}" not found.\n\nUse list_notebooks to see available folders and their IDs.`;
|
|
42
|
+
}
|
|
43
|
+
// Prepare the update body - only include fields that are being updated
|
|
44
|
+
const updateBody = {};
|
|
45
|
+
if (options.title !== undefined)
|
|
46
|
+
updateBody.title = options.title.trim();
|
|
47
|
+
if (options.parent_id !== undefined)
|
|
48
|
+
updateBody.parent_id = options.parent_id;
|
|
49
|
+
// Update the folder
|
|
50
|
+
const updatedFolder = await this.apiClient.put(`/folders/${options.folder_id}`, updateBody);
|
|
51
|
+
// Validate response
|
|
52
|
+
if (!updatedFolder || typeof updatedFolder !== "object" || !updatedFolder.id) {
|
|
53
|
+
return "Error: Unexpected response format from Joplin API when updating folder";
|
|
54
|
+
}
|
|
55
|
+
// Get parent folder info for both old and new locations if parent_id changed
|
|
56
|
+
let oldParentInfo = "Top level";
|
|
57
|
+
let newParentInfo = "Top level";
|
|
58
|
+
if (currentFolder.parent_id) {
|
|
59
|
+
try {
|
|
60
|
+
const oldParent = await this.apiClient.get(`/folders/${currentFolder.parent_id}`, {
|
|
61
|
+
query: { fields: "title" },
|
|
62
|
+
});
|
|
63
|
+
if (oldParent?.title) {
|
|
64
|
+
oldParentInfo = `Inside "${oldParent.title}"`;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
oldParentInfo = `Parent ID: ${currentFolder.parent_id}`;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (updatedFolder.parent_id && updatedFolder.parent_id !== currentFolder.parent_id) {
|
|
72
|
+
try {
|
|
73
|
+
const newParent = await this.apiClient.get(`/folders/${updatedFolder.parent_id}`, {
|
|
74
|
+
query: { fields: "title" },
|
|
75
|
+
});
|
|
76
|
+
if (newParent?.title) {
|
|
77
|
+
newParentInfo = `Inside "${newParent.title}"`;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
newParentInfo = `Parent ID: ${updatedFolder.parent_id}`;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
else if (updatedFolder.parent_id) {
|
|
85
|
+
newParentInfo = oldParentInfo;
|
|
86
|
+
}
|
|
87
|
+
// Format success response with before/after comparison
|
|
88
|
+
const resultLines = [];
|
|
89
|
+
resultLines.push(`✅ Successfully updated notebook!`);
|
|
90
|
+
resultLines.push("");
|
|
91
|
+
resultLines.push(`📁 Notebook: "${updatedFolder.title}"`);
|
|
92
|
+
resultLines.push(` Folder ID: ${updatedFolder.id}`);
|
|
93
|
+
resultLines.push("");
|
|
94
|
+
// Show what changed
|
|
95
|
+
resultLines.push(`🔄 Changes made:`);
|
|
96
|
+
if (options.title !== undefined && currentFolder.title !== updatedFolder.title) {
|
|
97
|
+
resultLines.push(` Title: "${currentFolder.title}" → "${updatedFolder.title}"`);
|
|
98
|
+
}
|
|
99
|
+
if (options.parent_id !== undefined && currentFolder.parent_id !== updatedFolder.parent_id) {
|
|
100
|
+
resultLines.push(` Location: ${oldParentInfo} → ${newParentInfo}`);
|
|
101
|
+
}
|
|
102
|
+
if (updatedFolder.updated_time) {
|
|
103
|
+
const updatedTime = this.formatDate(updatedFolder.updated_time);
|
|
104
|
+
resultLines.push(` Last Updated: ${updatedTime}`);
|
|
105
|
+
}
|
|
106
|
+
resultLines.push("");
|
|
107
|
+
resultLines.push(`🔗 Next steps:`);
|
|
108
|
+
resultLines.push(` - View notebook: read_notebook notebook_id="${updatedFolder.id}"`);
|
|
109
|
+
resultLines.push(` - View all notebooks: list_notebooks`);
|
|
110
|
+
if (updatedFolder.parent_id) {
|
|
111
|
+
resultLines.push(` - View parent notebook: read_notebook notebook_id="${updatedFolder.parent_id}"`);
|
|
112
|
+
}
|
|
113
|
+
return resultLines.join("\n");
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
if (error.response) {
|
|
117
|
+
if (error.response.status === 404) {
|
|
118
|
+
if (error.config?.url?.includes(`/folders/${options.folder_id}`)) {
|
|
119
|
+
return `Folder with ID "${options.folder_id}" not found.\n\nUse list_notebooks to see available folders and their IDs.`;
|
|
120
|
+
}
|
|
121
|
+
if (options.parent_id) {
|
|
122
|
+
return `Error: Parent folder with ID "${options.parent_id}" not found.\n\nUse list_notebooks to see available folders and their IDs.`;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (error.response.status === 400) {
|
|
126
|
+
return `Error updating folder: Invalid request data.\n\nPlease check your input parameters. ${error.response.data?.error || ""}`;
|
|
127
|
+
}
|
|
128
|
+
if (error.response.status === 409) {
|
|
129
|
+
return `Error: A folder with the title "${options.title}" might already exist in this location.\n\nTry a different title or check existing folders with list_notebooks.`;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return this.formatError(error, "updating folder");
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
export default EditFolder;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import BaseTool from "./base-tool.js";
|
|
2
|
+
interface EditNoteOptions {
|
|
3
|
+
note_id: string;
|
|
4
|
+
title?: string | undefined;
|
|
5
|
+
body?: string | undefined;
|
|
6
|
+
body_html?: string | undefined;
|
|
7
|
+
parent_id?: string | undefined;
|
|
8
|
+
is_todo?: boolean | undefined;
|
|
9
|
+
todo_completed?: boolean | undefined;
|
|
10
|
+
todo_due?: number | undefined;
|
|
11
|
+
}
|
|
12
|
+
declare class EditNote extends BaseTool {
|
|
13
|
+
call(options: EditNoteOptions): Promise<string>;
|
|
14
|
+
}
|
|
15
|
+
export default EditNote;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import BaseTool from "./base-tool.js";
|
|
2
|
+
class EditNote extends BaseTool {
|
|
3
|
+
async call(options) {
|
|
4
|
+
if (!options || typeof options !== "object") {
|
|
5
|
+
return 'Please provide note edit options. Example: edit_note {"note_id": "abc123", "title": "Updated Title"}';
|
|
6
|
+
}
|
|
7
|
+
// Validate required note_id
|
|
8
|
+
if (!options.note_id) {
|
|
9
|
+
return 'Please provide note edit options. Example: edit_note {"note_id": "abc123", "title": "Updated Title"}';
|
|
10
|
+
}
|
|
11
|
+
const noteIdError = this.validateId(options.note_id, "note");
|
|
12
|
+
if (noteIdError) {
|
|
13
|
+
return noteIdError;
|
|
14
|
+
}
|
|
15
|
+
// Validate that we have at least one field to update
|
|
16
|
+
const updateFields = ["title", "body", "body_html", "parent_id", "is_todo", "todo_completed", "todo_due"];
|
|
17
|
+
const hasUpdate = updateFields.some((field) => options[field] !== undefined);
|
|
18
|
+
if (!hasUpdate) {
|
|
19
|
+
return "Please provide at least one field to update. Available fields: title, body, body_html, parent_id, is_todo, todo_completed, todo_due";
|
|
20
|
+
}
|
|
21
|
+
// Validate parent_id if provided
|
|
22
|
+
if (options.parent_id !== undefined && options.parent_id !== null && options.parent_id !== "") {
|
|
23
|
+
if (options.parent_id.length < 10 || !options.parent_id.match(/[a-f0-9]/i)) {
|
|
24
|
+
return `Error: "${options.parent_id}" does not appear to be a valid notebook ID.\n\nNotebook IDs are long alphanumeric strings like "58a0a29f68bc4141b49c99f5d367638a".\n\nUse list_notebooks to see available notebooks and their IDs.`;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
// First, get the current note to show before/after comparison
|
|
29
|
+
const currentNote = await this.apiClient.get(`/notes/${options.note_id}`, {
|
|
30
|
+
query: { fields: "id,title,body,parent_id,is_todo,todo_completed,todo_due,updated_time" },
|
|
31
|
+
});
|
|
32
|
+
if (!currentNote || !currentNote.id) {
|
|
33
|
+
return `Note with ID "${options.note_id}" not found.\n\nUse search_notes to find notes and their IDs.`;
|
|
34
|
+
}
|
|
35
|
+
// Prepare the update body - only include fields that are being updated
|
|
36
|
+
const updateBody = {};
|
|
37
|
+
if (options.title !== undefined)
|
|
38
|
+
updateBody.title = options.title;
|
|
39
|
+
if (options.body !== undefined)
|
|
40
|
+
updateBody.body = options.body;
|
|
41
|
+
if (options.body_html !== undefined)
|
|
42
|
+
updateBody.body_html = options.body_html;
|
|
43
|
+
if (options.parent_id !== undefined)
|
|
44
|
+
updateBody.parent_id = options.parent_id;
|
|
45
|
+
if (options.is_todo !== undefined)
|
|
46
|
+
updateBody.is_todo = options.is_todo;
|
|
47
|
+
if (options.todo_completed !== undefined)
|
|
48
|
+
updateBody.todo_completed = options.todo_completed;
|
|
49
|
+
if (options.todo_due !== undefined)
|
|
50
|
+
updateBody.todo_due = options.todo_due;
|
|
51
|
+
// Update the note
|
|
52
|
+
const updatedNote = await this.apiClient.put(`/notes/${options.note_id}`, updateBody);
|
|
53
|
+
// Validate response
|
|
54
|
+
if (!updatedNote || typeof updatedNote !== "object" || !updatedNote.id) {
|
|
55
|
+
return "Error: Unexpected response format from Joplin API when updating note";
|
|
56
|
+
}
|
|
57
|
+
// Get notebook info for both old and new locations if parent_id changed
|
|
58
|
+
let oldNotebookInfo = "Root level";
|
|
59
|
+
let newNotebookInfo = "Root level";
|
|
60
|
+
if (currentNote.parent_id) {
|
|
61
|
+
try {
|
|
62
|
+
const oldNotebook = await this.apiClient.get(`/folders/${currentNote.parent_id}`, {
|
|
63
|
+
query: { fields: "title" },
|
|
64
|
+
});
|
|
65
|
+
if (oldNotebook?.title) {
|
|
66
|
+
oldNotebookInfo = `"${oldNotebook.title}"`;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
oldNotebookInfo = `Notebook ID: ${currentNote.parent_id}`;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (updatedNote.parent_id && updatedNote.parent_id !== currentNote.parent_id) {
|
|
74
|
+
try {
|
|
75
|
+
const newNotebook = await this.apiClient.get(`/folders/${updatedNote.parent_id}`, {
|
|
76
|
+
query: { fields: "title" },
|
|
77
|
+
});
|
|
78
|
+
if (newNotebook?.title) {
|
|
79
|
+
newNotebookInfo = `"${newNotebook.title}"`;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
newNotebookInfo = `Notebook ID: ${updatedNote.parent_id}`;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
else if (updatedNote.parent_id) {
|
|
87
|
+
newNotebookInfo = oldNotebookInfo;
|
|
88
|
+
}
|
|
89
|
+
// Format success response with before/after comparison
|
|
90
|
+
const resultLines = [];
|
|
91
|
+
resultLines.push(`✅ Successfully updated note!`);
|
|
92
|
+
resultLines.push("");
|
|
93
|
+
resultLines.push(`📝 Note: "${updatedNote.title || "Untitled"}"`);
|
|
94
|
+
resultLines.push(` Note ID: ${updatedNote.id}`);
|
|
95
|
+
resultLines.push("");
|
|
96
|
+
// Show what changed
|
|
97
|
+
resultLines.push(`🔄 Changes made:`);
|
|
98
|
+
if (options.title !== undefined && currentNote.title !== updatedNote.title) {
|
|
99
|
+
resultLines.push(` Title: "${currentNote.title}" → "${updatedNote.title}"`);
|
|
100
|
+
}
|
|
101
|
+
if (options.parent_id !== undefined && currentNote.parent_id !== updatedNote.parent_id) {
|
|
102
|
+
resultLines.push(` Location: ${oldNotebookInfo} → ${newNotebookInfo}`);
|
|
103
|
+
}
|
|
104
|
+
if (options.is_todo !== undefined && currentNote.is_todo !== updatedNote.is_todo) {
|
|
105
|
+
const oldType = currentNote.is_todo ? "Todo" : "Regular note";
|
|
106
|
+
const newType = updatedNote.is_todo ? "Todo" : "Regular note";
|
|
107
|
+
resultLines.push(` Type: ${oldType} → ${newType}`);
|
|
108
|
+
}
|
|
109
|
+
if (options.todo_completed !== undefined && currentNote.todo_completed !== updatedNote.todo_completed) {
|
|
110
|
+
const oldStatus = currentNote.todo_completed ? "Completed" : "Not completed";
|
|
111
|
+
const newStatus = updatedNote.todo_completed ? "Completed" : "Not completed";
|
|
112
|
+
resultLines.push(` Todo Status: ${oldStatus} → ${newStatus}`);
|
|
113
|
+
}
|
|
114
|
+
if (options.todo_due !== undefined) {
|
|
115
|
+
const oldDue = currentNote.todo_due ? this.formatDate(currentNote.todo_due) : "No due date";
|
|
116
|
+
const newDue = updatedNote.todo_due ? this.formatDate(updatedNote.todo_due) : "No due date";
|
|
117
|
+
if (oldDue !== newDue) {
|
|
118
|
+
resultLines.push(` Due Date: ${oldDue} → ${newDue}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (options.body !== undefined) {
|
|
122
|
+
resultLines.push(` Content: Updated`);
|
|
123
|
+
}
|
|
124
|
+
if (options.body_html !== undefined) {
|
|
125
|
+
resultLines.push(` HTML Content: Updated`);
|
|
126
|
+
}
|
|
127
|
+
const updatedTime = this.formatDate(updatedNote.updated_time);
|
|
128
|
+
resultLines.push(` Last Updated: ${updatedTime}`);
|
|
129
|
+
resultLines.push("");
|
|
130
|
+
resultLines.push(`🔗 Next steps:`);
|
|
131
|
+
resultLines.push(` - Read the note: read_note note_id="${updatedNote.id}"`);
|
|
132
|
+
if (updatedNote.parent_id) {
|
|
133
|
+
resultLines.push(` - View notebook: read_notebook notebook_id="${updatedNote.parent_id}"`);
|
|
134
|
+
}
|
|
135
|
+
return resultLines.join("\n");
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
if (error.response) {
|
|
139
|
+
if (error.response.status === 404) {
|
|
140
|
+
return `Note with ID "${options.note_id}" not found.\n\nUse search_notes to find notes and their IDs.`;
|
|
141
|
+
}
|
|
142
|
+
if (error.response.status === 400) {
|
|
143
|
+
return `Error updating note: Invalid request data.\n\nPlease check your input parameters. ${error.response.data?.error || ""}`;
|
|
144
|
+
}
|
|
145
|
+
if (error.response.status === 404 && options.parent_id) {
|
|
146
|
+
return `Error: Notebook with ID "${options.parent_id}" not found.\n\nUse list_notebooks to see available notebooks and their IDs.`;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return this.formatError(error, "updating note");
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
export default EditNote;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import ListNotebooks from "./list-notebooks.js";
|
|
2
|
+
import SearchNotes from "./search-notes.js";
|
|
3
|
+
import ReadNotebook from "./read-notebook.js";
|
|
4
|
+
import ReadNote from "./read-note.js";
|
|
5
|
+
import ReadMultiNote from "./read-multi-note.js";
|
|
6
|
+
import CreateNote from "./create-note.js";
|
|
7
|
+
import CreateFolder from "./create-folder.js";
|
|
8
|
+
import EditNote from "./edit-note.js";
|
|
9
|
+
import EditFolder from "./edit-folder.js";
|
|
10
|
+
import DeleteNote from "./delete-note.js";
|
|
11
|
+
import DeleteFolder from "./delete-folder.js";
|
|
12
|
+
export { ListNotebooks, SearchNotes, ReadNotebook, ReadNote, ReadMultiNote, CreateNote, CreateFolder, EditNote, EditFolder, DeleteNote, DeleteFolder, };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import ListNotebooks from "./list-notebooks.js";
|
|
2
|
+
import SearchNotes from "./search-notes.js";
|
|
3
|
+
import ReadNotebook from "./read-notebook.js";
|
|
4
|
+
import ReadNote from "./read-note.js";
|
|
5
|
+
import ReadMultiNote from "./read-multi-note.js";
|
|
6
|
+
import CreateNote from "./create-note.js";
|
|
7
|
+
import CreateFolder from "./create-folder.js";
|
|
8
|
+
import EditNote from "./edit-note.js";
|
|
9
|
+
import EditFolder from "./edit-folder.js";
|
|
10
|
+
import DeleteNote from "./delete-note.js";
|
|
11
|
+
import DeleteFolder from "./delete-folder.js";
|
|
12
|
+
export { ListNotebooks, SearchNotes, ReadNotebook, ReadNote, ReadMultiNote, CreateNote, CreateFolder, EditNote, EditFolder, DeleteNote, DeleteFolder, };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import BaseTool from "./base-tool.js";
|
|
2
|
+
class ListNotebooks extends BaseTool {
|
|
3
|
+
async call() {
|
|
4
|
+
try {
|
|
5
|
+
const notebooks = await this.apiClient.getAllItems("/folders", {
|
|
6
|
+
query: { fields: "id,title,parent_id" },
|
|
7
|
+
});
|
|
8
|
+
const notebooksByParentId = {};
|
|
9
|
+
notebooks.forEach((notebook) => {
|
|
10
|
+
const parentId = notebook.parent_id || "";
|
|
11
|
+
if (!notebooksByParentId[parentId]) {
|
|
12
|
+
notebooksByParentId[parentId] = [];
|
|
13
|
+
}
|
|
14
|
+
notebooksByParentId[parentId].push(notebook);
|
|
15
|
+
});
|
|
16
|
+
// Add a header with instructions
|
|
17
|
+
const resultLines = [
|
|
18
|
+
"Joplin Notebooks:\n",
|
|
19
|
+
"NOTE: To read a notebook, use the notebook_id with the read_notebook command\n",
|
|
20
|
+
'Example: read_notebook notebook_id="your-notebook-id"\n\n',
|
|
21
|
+
];
|
|
22
|
+
// Add the notebook hierarchy
|
|
23
|
+
resultLines.push(...this.notebooksLines(notebooksByParentId[""] || [], {
|
|
24
|
+
indent: 0,
|
|
25
|
+
notebooksByParentId,
|
|
26
|
+
}));
|
|
27
|
+
return resultLines.join("");
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
return this.formatError(error, "listing notebooks");
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
notebooksLines(notebooks, { indent = 0, notebooksByParentId, }) {
|
|
34
|
+
const result = [];
|
|
35
|
+
const indentSpaces = " ".repeat(indent);
|
|
36
|
+
this.sortNotebooks(notebooks).forEach((notebook) => {
|
|
37
|
+
const id = notebook.id;
|
|
38
|
+
result.push(`${indentSpaces}Notebook: "${notebook.title}" (notebook_id: "${id}")\n`);
|
|
39
|
+
const childNotebooks = notebooksByParentId[id];
|
|
40
|
+
if (childNotebooks) {
|
|
41
|
+
result.push(...this.notebooksLines(childNotebooks, {
|
|
42
|
+
indent: indent + 2,
|
|
43
|
+
notebooksByParentId,
|
|
44
|
+
}));
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
sortNotebooks(notebooks) {
|
|
50
|
+
// Ensure that notebooks starting with '[0]' are sorted first
|
|
51
|
+
const CHARACTER_BEFORE_A = String.fromCharCode("A".charCodeAt(0) - 1);
|
|
52
|
+
return [...notebooks].sort((a, b) => {
|
|
53
|
+
const titleA = a.title.replace("[", CHARACTER_BEFORE_A);
|
|
54
|
+
const titleB = b.title.replace("[", CHARACTER_BEFORE_A);
|
|
55
|
+
return titleA.localeCompare(titleB);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export default ListNotebooks;
|