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.
Files changed (71) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +384 -0
  3. package/dist/bin/cli.d.ts +2 -0
  4. package/dist/bin/cli.js +7 -0
  5. package/dist/index.d.ts +1 -0
  6. package/dist/index.js +204 -0
  7. package/dist/lib/joplin-api-client.d.ts +23 -0
  8. package/dist/lib/joplin-api-client.js +110 -0
  9. package/dist/lib/logger.d.ts +21 -0
  10. package/dist/lib/logger.js +68 -0
  11. package/dist/lib/parse-args.d.ts +2 -0
  12. package/dist/lib/parse-args.js +81 -0
  13. package/dist/lib/tools/base-tool.d.ts +27 -0
  14. package/dist/lib/tools/base-tool.js +24 -0
  15. package/dist/lib/tools/create-folder.d.ts +9 -0
  16. package/dist/lib/tools/create-folder.js +79 -0
  17. package/dist/lib/tools/create-note.d.ts +13 -0
  18. package/dist/lib/tools/create-note.js +88 -0
  19. package/dist/lib/tools/delete-folder.d.ts +10 -0
  20. package/dist/lib/tools/delete-folder.js +138 -0
  21. package/dist/lib/tools/delete-note.d.ts +9 -0
  22. package/dist/lib/tools/delete-note.js +92 -0
  23. package/dist/lib/tools/edit-folder.d.ts +10 -0
  24. package/dist/lib/tools/edit-folder.js +136 -0
  25. package/dist/lib/tools/edit-note.d.ts +15 -0
  26. package/dist/lib/tools/edit-note.js +153 -0
  27. package/dist/lib/tools/index.d.ts +12 -0
  28. package/dist/lib/tools/index.js +12 -0
  29. package/dist/lib/tools/list-notebooks.d.ts +7 -0
  30. package/dist/lib/tools/list-notebooks.js +59 -0
  31. package/dist/lib/tools/read-multi-note.d.ts +5 -0
  32. package/dist/lib/tools/read-multi-note.js +108 -0
  33. package/dist/lib/tools/read-note.d.ts +5 -0
  34. package/dist/lib/tools/read-note.js +80 -0
  35. package/dist/lib/tools/read-notebook.d.ts +5 -0
  36. package/dist/lib/tools/read-notebook.js +66 -0
  37. package/dist/lib/tools/search-notes.d.ts +5 -0
  38. package/dist/lib/tools/search-notes.js +68 -0
  39. package/dist/tests/integration/joplin-integration.test.d.ts +1 -0
  40. package/dist/tests/integration/joplin-integration.test.js +117 -0
  41. package/dist/tests/manual/create-folder.test.d.ts +1 -0
  42. package/dist/tests/manual/create-folder.test.js +81 -0
  43. package/dist/tests/manual/create-note.test.d.ts +1 -0
  44. package/dist/tests/manual/create-note.test.js +84 -0
  45. package/dist/tests/manual/delete-folder.test.d.ts +1 -0
  46. package/dist/tests/manual/delete-folder.test.js +118 -0
  47. package/dist/tests/manual/delete-note.test.d.ts +1 -0
  48. package/dist/tests/manual/delete-note.test.js +101 -0
  49. package/dist/tests/manual/edit-folder.test.d.ts +1 -0
  50. package/dist/tests/manual/edit-folder.test.js +104 -0
  51. package/dist/tests/manual/edit-note.test.d.ts +1 -0
  52. package/dist/tests/manual/edit-note.test.js +118 -0
  53. package/dist/tests/manual/list-notebooks.test.d.ts +1 -0
  54. package/dist/tests/manual/list-notebooks.test.js +42 -0
  55. package/dist/tests/manual/read-note.test.d.ts +1 -0
  56. package/dist/tests/manual/read-note.test.js +54 -0
  57. package/dist/tests/manual/search-notes.test.d.ts +1 -0
  58. package/dist/tests/manual/search-notes.test.js +43 -0
  59. package/dist/tests/unit/create-tools.test.d.ts +1 -0
  60. package/dist/tests/unit/create-tools.test.js +223 -0
  61. package/dist/tests/unit/delete-tools.test.d.ts +1 -0
  62. package/dist/tests/unit/delete-tools.test.js +225 -0
  63. package/dist/tests/unit/edit-tools.test.d.ts +1 -0
  64. package/dist/tests/unit/edit-tools.test.js +261 -0
  65. package/dist/tests/unit/joplin-api-client.test.d.ts +1 -0
  66. package/dist/tests/unit/joplin-api-client.test.js +154 -0
  67. package/dist/vitest.config.d.ts +2 -0
  68. package/dist/vitest.config.js +22 -0
  69. package/dist/vitest.setup.d.ts +1 -0
  70. package/dist/vitest.setup.js +24 -0
  71. 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,9 @@
1
+ import BaseTool from "./base-tool.js";
2
+ interface DeleteNoteOptions {
3
+ note_id: string;
4
+ confirm?: boolean | undefined;
5
+ }
6
+ declare class DeleteNote extends BaseTool {
7
+ call(options: DeleteNoteOptions): Promise<string>;
8
+ }
9
+ export default DeleteNote;
@@ -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,7 @@
1
+ import BaseTool from "./base-tool.js";
2
+ declare class ListNotebooks extends BaseTool {
3
+ call(): Promise<string>;
4
+ private notebooksLines;
5
+ private sortNotebooks;
6
+ }
7
+ export default ListNotebooks;
@@ -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;
@@ -0,0 +1,5 @@
1
+ import BaseTool from "./base-tool.js";
2
+ declare class ReadMultiNote extends BaseTool {
3
+ call(noteIds: string[]): Promise<string>;
4
+ }
5
+ export default ReadMultiNote;