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,108 @@
|
|
|
1
|
+
import BaseTool from "./base-tool.js";
|
|
2
|
+
class ReadMultiNote extends BaseTool {
|
|
3
|
+
async call(noteIds) {
|
|
4
|
+
if (!noteIds || !Array.isArray(noteIds) || noteIds.length === 0) {
|
|
5
|
+
return 'Please provide an array of note IDs. Example: read_multinote note_ids=["id1", "id2", "id3"]';
|
|
6
|
+
}
|
|
7
|
+
// Validate that all IDs look like valid note IDs
|
|
8
|
+
const invalidIds = noteIds.filter((id) => !id || id.length < 10 || !id.match(/[a-f0-9]/i));
|
|
9
|
+
if (invalidIds.length > 0) {
|
|
10
|
+
return `Error: Some IDs do not appear to be valid note IDs: ${invalidIds.join(", ")}\n\nNote IDs are long alphanumeric strings like "58a0a29f68bc4141b49c99f5d367638a".\n\nUse search_notes to find notes and their IDs.`;
|
|
11
|
+
}
|
|
12
|
+
const resultLines = [];
|
|
13
|
+
const notFound = [];
|
|
14
|
+
const errors = [];
|
|
15
|
+
const successful = [];
|
|
16
|
+
// Add a header
|
|
17
|
+
resultLines.push(`# Reading ${noteIds.length} notes\n`);
|
|
18
|
+
// Process each note ID
|
|
19
|
+
for (let i = 0; i < noteIds.length; i++) {
|
|
20
|
+
const noteId = noteIds[i];
|
|
21
|
+
resultLines.push(`## Note ${i + 1} of ${noteIds.length} (ID: ${noteId})\n`);
|
|
22
|
+
try {
|
|
23
|
+
// Get the note details with all relevant fields
|
|
24
|
+
const note = await this.apiClient.get(`/notes/${noteId}`, {
|
|
25
|
+
query: {
|
|
26
|
+
fields: "id,title,body,parent_id,created_time,updated_time,is_todo,todo_completed,todo_due",
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
// Validate note response
|
|
30
|
+
if (!note || typeof note !== "object" || !note.id) {
|
|
31
|
+
errors.push(noteId);
|
|
32
|
+
resultLines.push(`Error: Unexpected response format from Joplin API when fetching note ${noteId}\n`);
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
successful.push(noteId);
|
|
36
|
+
// Get the notebook info to show where this note is located
|
|
37
|
+
let notebookInfo = "Unknown notebook";
|
|
38
|
+
if (note.parent_id) {
|
|
39
|
+
try {
|
|
40
|
+
const notebook = await this.apiClient.get(`/folders/${note.parent_id}`, {
|
|
41
|
+
query: { fields: "id,title" },
|
|
42
|
+
});
|
|
43
|
+
if (notebook && notebook.title) {
|
|
44
|
+
notebookInfo = `"${notebook.title}" (notebook_id: "${note.parent_id}")`;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
process.stderr.write(`Error fetching notebook info for note ${noteId}: ${error}\n`);
|
|
49
|
+
// Continue even if we can't get the notebook info
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// Add note metadata
|
|
53
|
+
resultLines.push(`### Note: "${note.title}"`);
|
|
54
|
+
resultLines.push(`Notebook: ${notebookInfo}`);
|
|
55
|
+
// Add todo status if applicable
|
|
56
|
+
if (note.is_todo) {
|
|
57
|
+
const status = note.todo_completed ? "Completed" : "Not completed";
|
|
58
|
+
resultLines.push(`Status: ${status}`);
|
|
59
|
+
if (note.todo_due) {
|
|
60
|
+
const dueDate = this.formatDate(note.todo_due);
|
|
61
|
+
resultLines.push(`Due: ${dueDate}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// Add timestamps
|
|
65
|
+
const createdDate = this.formatDate(note.created_time);
|
|
66
|
+
const updatedDate = this.formatDate(note.updated_time);
|
|
67
|
+
resultLines.push(`Created: ${createdDate}`);
|
|
68
|
+
resultLines.push(`Updated: ${updatedDate}`);
|
|
69
|
+
// Add a separator before the note content
|
|
70
|
+
resultLines.push("\n---\n");
|
|
71
|
+
// Add the note body
|
|
72
|
+
if (note.body) {
|
|
73
|
+
resultLines.push(note.body);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
resultLines.push("(This note has no content)");
|
|
77
|
+
}
|
|
78
|
+
// Add a separator after the note
|
|
79
|
+
resultLines.push("\n---\n");
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
process.stderr.write(`Error reading note ${noteId}: ${error}\n`);
|
|
83
|
+
if (error.response && error.response.status === 404) {
|
|
84
|
+
notFound.push(noteId);
|
|
85
|
+
resultLines.push(`Note with ID "${noteId}" not found.\n`);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
errors.push(noteId);
|
|
89
|
+
resultLines.push(`Error reading note: ${error.message || "Unknown error"}\n`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// Add a summary at the end
|
|
94
|
+
resultLines.push("# Summary");
|
|
95
|
+
resultLines.push(`Total notes requested: ${noteIds.length}`);
|
|
96
|
+
resultLines.push(`Successfully retrieved: ${successful.length}`);
|
|
97
|
+
if (notFound.length > 0) {
|
|
98
|
+
resultLines.push(`Notes not found: ${notFound.length}`);
|
|
99
|
+
resultLines.push(`IDs not found: ${notFound.join(", ")}`);
|
|
100
|
+
}
|
|
101
|
+
if (errors.length > 0) {
|
|
102
|
+
resultLines.push(`Errors encountered: ${errors.length}`);
|
|
103
|
+
resultLines.push(`IDs with errors: ${errors.join(", ")}`);
|
|
104
|
+
}
|
|
105
|
+
return resultLines.join("\n");
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
export default ReadMultiNote;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import BaseTool from "./base-tool.js";
|
|
2
|
+
class ReadNote extends BaseTool {
|
|
3
|
+
async call(noteId) {
|
|
4
|
+
const validationError = this.validateId(noteId, "note");
|
|
5
|
+
if (validationError) {
|
|
6
|
+
return validationError;
|
|
7
|
+
}
|
|
8
|
+
try {
|
|
9
|
+
// Get the note details with all relevant fields
|
|
10
|
+
const note = await this.apiClient.get(`/notes/${noteId}`, {
|
|
11
|
+
query: {
|
|
12
|
+
fields: "id,title,body,parent_id,created_time,updated_time,is_todo,todo_completed,todo_due",
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
// Validate note response
|
|
16
|
+
if (!note || typeof note !== "object" || !note.id) {
|
|
17
|
+
return `Error: Unexpected response format from Joplin API when fetching note`;
|
|
18
|
+
}
|
|
19
|
+
// Get the notebook info to show where this note is located
|
|
20
|
+
let notebookInfo = "Unknown notebook";
|
|
21
|
+
if (note.parent_id) {
|
|
22
|
+
try {
|
|
23
|
+
const notebook = await this.apiClient.get(`/folders/${note.parent_id}`, {
|
|
24
|
+
query: { fields: "id,title" },
|
|
25
|
+
});
|
|
26
|
+
if (notebook && notebook.title) {
|
|
27
|
+
notebookInfo = `"${notebook.title}" (notebook_id: "${note.parent_id}")`;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
process.stderr.write(`Error fetching notebook info: ${error}\n`);
|
|
32
|
+
// Continue even if we can't get the notebook info
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// Format the note content
|
|
36
|
+
const resultLines = [];
|
|
37
|
+
// Add note header with metadata
|
|
38
|
+
resultLines.push(`# Note: "${note.title}"`);
|
|
39
|
+
resultLines.push(`Note ID: ${note.id}`);
|
|
40
|
+
resultLines.push(`Notebook: ${notebookInfo}`);
|
|
41
|
+
// Add todo status if applicable
|
|
42
|
+
if (note.is_todo) {
|
|
43
|
+
const status = note.todo_completed ? "Completed" : "Not completed";
|
|
44
|
+
resultLines.push(`Status: ${status}`);
|
|
45
|
+
if (note.todo_due) {
|
|
46
|
+
const dueDate = this.formatDate(note.todo_due);
|
|
47
|
+
resultLines.push(`Due: ${dueDate}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// Add timestamps
|
|
51
|
+
const createdDate = this.formatDate(note.created_time);
|
|
52
|
+
const updatedDate = this.formatDate(note.updated_time);
|
|
53
|
+
resultLines.push(`Created: ${createdDate}`);
|
|
54
|
+
resultLines.push(`Updated: ${updatedDate}`);
|
|
55
|
+
// Add a separator before the note content
|
|
56
|
+
resultLines.push("\n---\n");
|
|
57
|
+
// Add the note body
|
|
58
|
+
if (note.body) {
|
|
59
|
+
resultLines.push(note.body);
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
resultLines.push("(This note has no content)");
|
|
63
|
+
}
|
|
64
|
+
// Add a footer with helpful commands
|
|
65
|
+
resultLines.push("\n---\n");
|
|
66
|
+
resultLines.push("Related commands:");
|
|
67
|
+
resultLines.push(`- To view the notebook containing this note: read_notebook notebook_id="${note.parent_id}"`);
|
|
68
|
+
resultLines.push('- To search for more notes: search_notes query="your search term"');
|
|
69
|
+
return resultLines.join("\n");
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
if (error.response && error.response.status === 404) {
|
|
73
|
+
return `Note with ID "${noteId}" not found.\n\nThis might happen if:\n1. The ID is incorrect\n2. You're using a notebook ID instead of a note ID\n3. The note has been deleted\n\nUse search_notes to find notes and their IDs.`;
|
|
74
|
+
}
|
|
75
|
+
return (this.formatError(error, "reading note") +
|
|
76
|
+
`\n\nMake sure you're using a valid note ID.\nUse search_notes to find notes and their IDs.`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
export default ReadNote;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import BaseTool from "./base-tool.js";
|
|
2
|
+
class ReadNotebook extends BaseTool {
|
|
3
|
+
async call(notebookId) {
|
|
4
|
+
const validationError = this.validateId(notebookId, "notebook");
|
|
5
|
+
if (validationError) {
|
|
6
|
+
return validationError;
|
|
7
|
+
}
|
|
8
|
+
try {
|
|
9
|
+
// First, get the notebook details
|
|
10
|
+
const notebook = await this.apiClient.get(`/folders/${notebookId}`, {
|
|
11
|
+
query: { fields: "id,title,parent_id" },
|
|
12
|
+
});
|
|
13
|
+
// Validate notebook response
|
|
14
|
+
if (!notebook || typeof notebook !== "object" || !notebook.id) {
|
|
15
|
+
return `Error: Unexpected response format from Joplin API when fetching notebook`;
|
|
16
|
+
}
|
|
17
|
+
// Get all notes in this notebook
|
|
18
|
+
const notes = await this.apiClient.get(`/folders/${notebookId}/notes`, {
|
|
19
|
+
query: { fields: "id,title,updated_time,is_todo,todo_completed" },
|
|
20
|
+
});
|
|
21
|
+
// Validate notes response
|
|
22
|
+
if (!notes || typeof notes !== "object") {
|
|
23
|
+
return `Error: Unexpected response format from Joplin API when fetching notes`;
|
|
24
|
+
}
|
|
25
|
+
if (!notes.items || !Array.isArray(notes.items) || notes.items.length === 0) {
|
|
26
|
+
return `Notebook "${notebook.title}" (notebook_id: "${notebook.id}") is empty.\n\nTry another notebook ID or use list_notebooks to see all available notebooks.`;
|
|
27
|
+
}
|
|
28
|
+
// Format the notebook contents
|
|
29
|
+
const resultLines = [];
|
|
30
|
+
resultLines.push(`# Notebook: "${notebook.title}" (notebook_id: "${notebook.id}")`);
|
|
31
|
+
resultLines.push(`Contains ${notes.items.length} notes:\n`);
|
|
32
|
+
resultLines.push(`NOTE: This is showing the contents of notebook "${notebook.title}", not a specific note.\n`);
|
|
33
|
+
// If multiple notes were found, add a hint about read_multinote
|
|
34
|
+
if (notes.items.length > 1) {
|
|
35
|
+
const noteIds = notes.items.map((note) => note.id);
|
|
36
|
+
resultLines.push(`TIP: To read all ${notes.items.length} notes at once, use:\n`);
|
|
37
|
+
resultLines.push(`read_multinote note_ids=${JSON.stringify(noteIds)}\n`);
|
|
38
|
+
}
|
|
39
|
+
// Sort notes by updated_time (newest first)
|
|
40
|
+
const sortedNotes = [...notes.items].sort((a, b) => b.updated_time - a.updated_time);
|
|
41
|
+
sortedNotes.forEach((note) => {
|
|
42
|
+
const updatedDate = this.formatDate(note.updated_time);
|
|
43
|
+
// Add checkbox for todos
|
|
44
|
+
if (note.is_todo) {
|
|
45
|
+
const checkboxStatus = note.todo_completed ? "ā
" : "ā";
|
|
46
|
+
resultLines.push(`- ${checkboxStatus} Note: "${note.title}" (note_id: "${note.id}")`);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
resultLines.push(`- Note: "${note.title}" (note_id: "${note.id}")`);
|
|
50
|
+
}
|
|
51
|
+
resultLines.push(` Updated: ${updatedDate}`);
|
|
52
|
+
resultLines.push(` To read this note: read_note note_id="${note.id}"`);
|
|
53
|
+
resultLines.push(""); // Empty line between notes
|
|
54
|
+
});
|
|
55
|
+
return resultLines.join("\n");
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
if (error.response && error.response.status === 404) {
|
|
59
|
+
return `Notebook with ID "${notebookId}" not found.\n\nThis might happen if:\n1. The ID is incorrect\n2. You're using a note title instead of a notebook ID\n3. The notebook has been deleted\n\nUse list_notebooks to see all available notebooks with their IDs.`;
|
|
60
|
+
}
|
|
61
|
+
return (this.formatError(error, "reading notebook") +
|
|
62
|
+
`\n\nMake sure you're using a valid notebook ID, not a note title.\nUse list_notebooks to see all available notebooks with their IDs.`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
export default ReadNotebook;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import BaseTool from "./base-tool.js";
|
|
2
|
+
class SearchNotes extends BaseTool {
|
|
3
|
+
async call(query) {
|
|
4
|
+
if (!query) {
|
|
5
|
+
return "Please provide a search query.";
|
|
6
|
+
}
|
|
7
|
+
try {
|
|
8
|
+
// Search for notes with the given query
|
|
9
|
+
const searchResults = await this.apiClient.get("/search", {
|
|
10
|
+
query: {
|
|
11
|
+
query,
|
|
12
|
+
fields: "id,title,body,parent_id,updated_time",
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
// Handle case where the API doesn't return the expected structure
|
|
16
|
+
if (!searchResults || typeof searchResults !== "object") {
|
|
17
|
+
return `Error: Unexpected response format from Joplin API`;
|
|
18
|
+
}
|
|
19
|
+
// Handle case where no items were found
|
|
20
|
+
if (!searchResults.items || !Array.isArray(searchResults.items) || searchResults.items.length === 0) {
|
|
21
|
+
return `No notes found matching query: "${query}"`;
|
|
22
|
+
}
|
|
23
|
+
// Get all folders to be able to show notebook names
|
|
24
|
+
const folders = await this.apiClient.getAllItems("/folders", {
|
|
25
|
+
query: {
|
|
26
|
+
fields: "id,title",
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
// Create a map of folder IDs to folder titles for quick lookup
|
|
30
|
+
const folderMap = {};
|
|
31
|
+
folders.forEach((folder) => {
|
|
32
|
+
folderMap[folder.id] = folder.title;
|
|
33
|
+
});
|
|
34
|
+
// Format the search results
|
|
35
|
+
const resultLines = [];
|
|
36
|
+
resultLines.push(`Found ${searchResults.items.length} notes matching query: "${query}"\n`);
|
|
37
|
+
resultLines.push(`NOTE: To read a notebook, use the notebook ID (not the note title)\n`);
|
|
38
|
+
// If multiple notes were found, add a hint about read_multinote
|
|
39
|
+
if (searchResults.items.length > 1) {
|
|
40
|
+
const noteIds = searchResults.items.map((note) => note.id);
|
|
41
|
+
resultLines.push(`TIP: To read all ${searchResults.items.length} notes at once, use:\n`);
|
|
42
|
+
resultLines.push(`read_multinote note_ids=${JSON.stringify(noteIds)}\n`);
|
|
43
|
+
}
|
|
44
|
+
searchResults.items.forEach((note) => {
|
|
45
|
+
const notebookTitle = folderMap[note.parent_id || ""] || "Unknown notebook";
|
|
46
|
+
const notebookId = note.parent_id || "unknown";
|
|
47
|
+
const updatedDate = this.formatDate(note.updated_time);
|
|
48
|
+
resultLines.push(`- Note: "${note.title}" (note_id: "${note.id}")`);
|
|
49
|
+
resultLines.push(` Notebook: "${notebookTitle}" (notebook_id: "${notebookId}")`);
|
|
50
|
+
resultLines.push(` Updated: ${updatedDate}`);
|
|
51
|
+
// Add a snippet of the note body if available
|
|
52
|
+
if (note.body) {
|
|
53
|
+
const snippet = note.body.substring(0, 100).replace(/\n/g, " ") + (note.body.length > 100 ? "..." : "");
|
|
54
|
+
resultLines.push(` Snippet: ${snippet}`);
|
|
55
|
+
}
|
|
56
|
+
// Add hints for using related commands
|
|
57
|
+
resultLines.push(` To read this note: read_note note_id="${note.id}"`);
|
|
58
|
+
resultLines.push(` To read this notebook: read_notebook notebook_id="${notebookId}"`);
|
|
59
|
+
resultLines.push(""); // Empty line between notes
|
|
60
|
+
});
|
|
61
|
+
return resultLines.join("\n");
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
return this.formatError(error, "searching notes");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
export default SearchNotes;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll } from "vitest";
|
|
2
|
+
import JoplinAPIClient from "../../lib/joplin-api-client.js";
|
|
3
|
+
import { ListNotebooks, SearchNotes, ReadNotebook } from "../../lib/tools/index.js";
|
|
4
|
+
describe("Joplin Integration Tests", () => {
|
|
5
|
+
let client;
|
|
6
|
+
let skipTests = false;
|
|
7
|
+
beforeAll(async () => {
|
|
8
|
+
// Check if we have the required environment variables for integration tests
|
|
9
|
+
if (!process.env.JOPLIN_TOKEN || !process.env.JOPLIN_PORT) {
|
|
10
|
+
console.warn("Skipping integration tests: JOPLIN_TOKEN or JOPLIN_PORT not set");
|
|
11
|
+
skipTests = true;
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
client = new JoplinAPIClient({
|
|
15
|
+
port: parseInt(process.env.JOPLIN_PORT),
|
|
16
|
+
token: process.env.JOPLIN_TOKEN,
|
|
17
|
+
});
|
|
18
|
+
// Check if Joplin is actually running and accessible
|
|
19
|
+
try {
|
|
20
|
+
const isAvailable = await client.serviceAvailable();
|
|
21
|
+
if (!isAvailable) {
|
|
22
|
+
console.warn("Skipping integration tests: Joplin service not available");
|
|
23
|
+
skipTests = true;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
console.warn("Skipping integration tests: Cannot connect to Joplin");
|
|
28
|
+
skipTests = true;
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
describe("Joplin API Client Integration", () => {
|
|
32
|
+
it("should connect to Joplin service", async () => {
|
|
33
|
+
if (skipTests)
|
|
34
|
+
return;
|
|
35
|
+
const isAvailable = await client.serviceAvailable();
|
|
36
|
+
expect(isAvailable).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
it("should fetch folders from Joplin", async () => {
|
|
39
|
+
if (skipTests)
|
|
40
|
+
return;
|
|
41
|
+
const folders = await client.get("/folders", {
|
|
42
|
+
query: { limit: 5, fields: "id,title,parent_id" },
|
|
43
|
+
});
|
|
44
|
+
expect(folders).toHaveProperty("items");
|
|
45
|
+
expect(Array.isArray(folders.items)).toBe(true);
|
|
46
|
+
expect(folders).toHaveProperty("has_more");
|
|
47
|
+
});
|
|
48
|
+
it("should fetch notes from Joplin", async () => {
|
|
49
|
+
if (skipTests)
|
|
50
|
+
return;
|
|
51
|
+
const notes = await client.get("/notes", {
|
|
52
|
+
query: { limit: 5, fields: "id,title,parent_id" },
|
|
53
|
+
});
|
|
54
|
+
expect(notes).toHaveProperty("items");
|
|
55
|
+
expect(Array.isArray(notes.items)).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
describe("Tools Integration", () => {
|
|
59
|
+
it("should list notebooks using ListNotebooks tool", async () => {
|
|
60
|
+
if (skipTests)
|
|
61
|
+
return;
|
|
62
|
+
const tool = new ListNotebooks(client);
|
|
63
|
+
const result = await tool.call();
|
|
64
|
+
expect(typeof result).toBe("string");
|
|
65
|
+
expect(result).toContain("Joplin Notebooks:");
|
|
66
|
+
expect(result).toContain("notebook_id:");
|
|
67
|
+
});
|
|
68
|
+
it("should search notes using SearchNotes tool", async () => {
|
|
69
|
+
if (skipTests)
|
|
70
|
+
return;
|
|
71
|
+
// First, let's check if there are any notes to search
|
|
72
|
+
const notes = await client.get("/notes", { query: { limit: 1 } });
|
|
73
|
+
if (notes.items.length === 0) {
|
|
74
|
+
console.warn("Skipping search test: No notes found in Joplin");
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const tool = new SearchNotes(client);
|
|
78
|
+
const result = await tool.call("test");
|
|
79
|
+
expect(typeof result).toBe("string");
|
|
80
|
+
// The result should either contain matching notes or indicate no matches
|
|
81
|
+
expect(result).toMatch(/(Found \d+ notes|No notes found)/);
|
|
82
|
+
});
|
|
83
|
+
it("should read a notebook using ReadNotebook tool", async () => {
|
|
84
|
+
if (skipTests)
|
|
85
|
+
return;
|
|
86
|
+
// First get a notebook ID
|
|
87
|
+
const folders = await client.get("/folders", {
|
|
88
|
+
query: { limit: 1, fields: "id,title" },
|
|
89
|
+
});
|
|
90
|
+
if (folders.items.length === 0) {
|
|
91
|
+
console.warn("Skipping read notebook test: No notebooks found");
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const notebookId = folders.items[0].id;
|
|
95
|
+
const tool = new ReadNotebook(client);
|
|
96
|
+
const result = await tool.call(notebookId);
|
|
97
|
+
expect(typeof result).toBe("string");
|
|
98
|
+
expect(result).toContain("Notebook:");
|
|
99
|
+
expect(result).toContain(notebookId);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
describe("Error Handling", () => {
|
|
103
|
+
it("should handle invalid API calls gracefully", async () => {
|
|
104
|
+
if (skipTests)
|
|
105
|
+
return;
|
|
106
|
+
await expect(client.get("/invalid-endpoint")).rejects.toThrow();
|
|
107
|
+
});
|
|
108
|
+
it("should handle invalid notebook ID in ReadNotebook tool", async () => {
|
|
109
|
+
if (skipTests)
|
|
110
|
+
return;
|
|
111
|
+
const tool = new ReadNotebook(client);
|
|
112
|
+
const result = await tool.call("invalid-notebook-id");
|
|
113
|
+
expect(typeof result).toBe("string");
|
|
114
|
+
expect(result).toContain("Error:");
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import dotenv from "dotenv";
|
|
2
|
+
import JoplinAPIClient from "../../lib/joplin-api-client.js";
|
|
3
|
+
import { CreateFolder, ListNotebooks } from "../../lib/tools/index.js";
|
|
4
|
+
// Load environment variables
|
|
5
|
+
dotenv.config();
|
|
6
|
+
// Check for required environment variables
|
|
7
|
+
const requiredEnvVars = ["JOPLIN_PORT", "JOPLIN_TOKEN"];
|
|
8
|
+
for (const envVar of requiredEnvVars) {
|
|
9
|
+
if (!process.env[envVar]) {
|
|
10
|
+
console.error(`Error: ${envVar} environment variable is required`);
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
// Create the Joplin API client
|
|
15
|
+
const apiClient = new JoplinAPIClient({
|
|
16
|
+
port: parseInt(process.env.JOPLIN_PORT),
|
|
17
|
+
token: process.env.JOPLIN_TOKEN,
|
|
18
|
+
});
|
|
19
|
+
// Create the tools
|
|
20
|
+
const createFolder = new CreateFolder(apiClient);
|
|
21
|
+
const listNotebooks = new ListNotebooks(apiClient);
|
|
22
|
+
// Test the create folder functionality
|
|
23
|
+
async function testCreateFolder() {
|
|
24
|
+
try {
|
|
25
|
+
// Check if Joplin is available
|
|
26
|
+
const available = await apiClient.serviceAvailable();
|
|
27
|
+
if (!available) {
|
|
28
|
+
console.error("Error: Joplin service is not available");
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
// Parse command line arguments
|
|
32
|
+
const args = process.argv.slice(2);
|
|
33
|
+
const title = args[0] || `Test Notebook ${new Date().toISOString()}`;
|
|
34
|
+
const parentId = args[1]; // Optional parent notebook ID
|
|
35
|
+
console.log("Creating notebook with:");
|
|
36
|
+
console.log(` Title: "${title}"`);
|
|
37
|
+
if (parentId) {
|
|
38
|
+
console.log(` Parent Notebook ID: "${parentId}"`);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
console.log(" Location: Top level (no parent specified)");
|
|
42
|
+
}
|
|
43
|
+
console.log("");
|
|
44
|
+
// Create the folder
|
|
45
|
+
const createOptions = { title };
|
|
46
|
+
if (parentId) {
|
|
47
|
+
createOptions.parent_id = parentId;
|
|
48
|
+
}
|
|
49
|
+
const result = await createFolder.call(createOptions);
|
|
50
|
+
console.log(result);
|
|
51
|
+
console.log("\nš Updated notebook hierarchy:");
|
|
52
|
+
const notebooks = await listNotebooks.call();
|
|
53
|
+
console.log(notebooks);
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
console.error("Error testing create folder:", error);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// Show usage if help requested
|
|
60
|
+
if (process.argv.includes("--help") || process.argv.includes("-h")) {
|
|
61
|
+
console.log(`
|
|
62
|
+
Usage: tsx tests/manual/create-folder.test.ts [title] [parent_id]
|
|
63
|
+
|
|
64
|
+
Arguments:
|
|
65
|
+
title - Notebook title (optional, defaults to timestamped title)
|
|
66
|
+
parent_id - ID of parent notebook to create subfolder in (optional, creates at top level if omitted)
|
|
67
|
+
|
|
68
|
+
Examples:
|
|
69
|
+
tsx tests/manual/create-folder.test.ts
|
|
70
|
+
tsx tests/manual/create-folder.test.ts "My Notebook"
|
|
71
|
+
tsx tests/manual/create-folder.test.ts "My Subfolder" "a1b2c3d4e5f6..."
|
|
72
|
+
|
|
73
|
+
First run list_notebooks to see available notebook IDs:
|
|
74
|
+
npm run test:manual:list-notebooks
|
|
75
|
+
`);
|
|
76
|
+
process.exit(0);
|
|
77
|
+
}
|
|
78
|
+
// Run if called directly
|
|
79
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
80
|
+
testCreateFolder();
|
|
81
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import dotenv from "dotenv";
|
|
2
|
+
import JoplinAPIClient from "../../lib/joplin-api-client.js";
|
|
3
|
+
import { CreateNote, ListNotebooks } from "../../lib/tools/index.js";
|
|
4
|
+
// Load environment variables
|
|
5
|
+
dotenv.config();
|
|
6
|
+
// Check for required environment variables
|
|
7
|
+
const requiredEnvVars = ["JOPLIN_PORT", "JOPLIN_TOKEN"];
|
|
8
|
+
for (const envVar of requiredEnvVars) {
|
|
9
|
+
if (!process.env[envVar]) {
|
|
10
|
+
console.error(`Error: ${envVar} environment variable is required`);
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
// Create the Joplin API client
|
|
15
|
+
const apiClient = new JoplinAPIClient({
|
|
16
|
+
port: parseInt(process.env.JOPLIN_PORT),
|
|
17
|
+
token: process.env.JOPLIN_TOKEN,
|
|
18
|
+
});
|
|
19
|
+
// Create the tools
|
|
20
|
+
const createNote = new CreateNote(apiClient);
|
|
21
|
+
const listNotebooks = new ListNotebooks(apiClient);
|
|
22
|
+
// Test the create note functionality
|
|
23
|
+
async function testCreateNote() {
|
|
24
|
+
try {
|
|
25
|
+
// Check if Joplin is available
|
|
26
|
+
const available = await apiClient.serviceAvailable();
|
|
27
|
+
if (!available) {
|
|
28
|
+
console.error("Error: Joplin service is not available");
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
// Parse command line arguments
|
|
32
|
+
const args = process.argv.slice(2);
|
|
33
|
+
const title = args[0] || `Test Note ${new Date().toISOString()}`;
|
|
34
|
+
const body = args[1] || `This is a test note created at ${new Date().toLocaleString()}`;
|
|
35
|
+
const notebookId = args[2]; // Optional notebook ID
|
|
36
|
+
console.log("Creating note with:");
|
|
37
|
+
console.log(` Title: "${title}"`);
|
|
38
|
+
console.log(` Body: "${body}"`);
|
|
39
|
+
if (notebookId) {
|
|
40
|
+
console.log(` Notebook ID: "${notebookId}"`);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
console.log(" Location: Root level (no notebook specified)");
|
|
44
|
+
}
|
|
45
|
+
console.log("");
|
|
46
|
+
// Create the note
|
|
47
|
+
const createOptions = { title, body };
|
|
48
|
+
if (notebookId) {
|
|
49
|
+
createOptions.parent_id = notebookId;
|
|
50
|
+
}
|
|
51
|
+
const result = await createNote.call(createOptions);
|
|
52
|
+
console.log(result);
|
|
53
|
+
console.log("\nš Available notebooks:");
|
|
54
|
+
const notebooks = await listNotebooks.call();
|
|
55
|
+
console.log(notebooks);
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
console.error("Error testing create note:", error);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// Show usage if help requested
|
|
62
|
+
if (process.argv.includes("--help") || process.argv.includes("-h")) {
|
|
63
|
+
console.log(`
|
|
64
|
+
Usage: tsx tests/manual/create-note.test.ts [title] [body] [notebook_id]
|
|
65
|
+
|
|
66
|
+
Arguments:
|
|
67
|
+
title - Note title (optional, defaults to timestamped title)
|
|
68
|
+
body - Note body content (optional, defaults to timestamped content)
|
|
69
|
+
notebook_id - ID of notebook to create note in (optional, creates in root if omitted)
|
|
70
|
+
|
|
71
|
+
Examples:
|
|
72
|
+
tsx tests/manual/create-note.test.ts
|
|
73
|
+
tsx tests/manual/create-note.test.ts "My Note" "My note content"
|
|
74
|
+
tsx tests/manual/create-note.test.ts "My Note" "My note content" "a1b2c3d4e5f6..."
|
|
75
|
+
|
|
76
|
+
First run list_notebooks to see available notebook IDs:
|
|
77
|
+
npm run test:manual:list-notebooks
|
|
78
|
+
`);
|
|
79
|
+
process.exit(0);
|
|
80
|
+
}
|
|
81
|
+
// Run if called directly
|
|
82
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
83
|
+
testCreateNote();
|
|
84
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|