diff --git a/README.md b/README.md
index f623b41..fefccb6 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,58 @@
 TriliumScripts
 ===============
 
-My collection of scripts for use with trilium
\ No newline at end of file
+Collection of scripts for use with trilium
+
+## syntax highlighting
+
+**highlight_moodlog.js** & **highlight_todo.js** - Both of these will apply appropriate syntaxy highlighting to the specific note.
+
+### installation
+
+1) add the code as the notes contents.
+
+2) set "note type" to "JS frontend"
+
+3) add an "owned attribute" of "#widget"
+
+### use
+
+on the note you want highlighting set the "owned attributes" to "#todotxt #hideHighlightWidget" or "#moodlog #hideHighlightWidget" depending on which syntax type you want to highlight
+
+## bidirectional sync
+
+**trilium_sync.php** - script to keep a folder of local text files and a trilium note in sync.
+
+### installation
+
+Edit the config values at the top of the script to match your trilium setup. Run it manually to test it is working as expected. If it is create a cron job.
+
+### example use
+
+```
+$> php trilium_sync.php 
+[i] checking connections
+[i] version: x.xx.x
+[i] gathering info
+
+- Folders -
+Folder/Path              Folder Name              Parent       noteId       Mod Trl    Mod Dsk    Status         
+Synced/                  Synced                   root         j0DRZhIzV97W 1742502936 1742507367 Exists
+Synced/BothPlaces/       BothPlaces               j0DRZhIzV97W AQb3Dg0yZHSu 1742507075 1742507200 Exists
+Synced/[del] tobe del/   [del] tobe del           j0DRZhIzV97W j0xUs1FdZL1Y 1742507321            Missing_Disk   
+Synced/AddToTrilium/     AddToTrilium                                                  1742507383 Missing_Trilium
+Synced/tobe deleted/     tobe deleted                                                  1742507200 Missing_Trilium
+- Files -
+Folder/Path              Filename                 Parent       noteId       Mod Trl    Mod Dsk    Hash  Status         
+Synced/BothPlaces/       UpdateDiskFile           AQb3Dg0yZHSu IwUZLwWdbiWG 1742507314 1742507199 Diff  Update_Disk    
+Synced/BothPlaces/       UpdateTriliumFile        AQb3Dg0yZHSu 7pMPgsb9AVRf 1742507130 1742507345 Diff  Update_Trilium 
+Synced/[del] tobe del/   gone_also.txt            j0xUs1FdZL1Y 4wOoHDy07csr 1742507178            N/A   Missing_Disk   
+Synced/AddToTrilium/     fromDiskToTrilium.txt                                         1742507394 N/A   Missing_Trilium
+Synced/tobe deleted/     gone_also.txt                                                 1742507200 N/A   Missing_Trilium
+
+[-] deleting disk folder: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/tobe deleted/
+[+] creating trilium folder: Synced/AddToTrilium/
+[+] creating trilium note: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/AddToTrilium/fromDiskToTrilium.txt
+[+] updating file on disk: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/BothPlaces/UpdateDiskFile
+[+] updating file on trilium: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/BothPlaces/UpdateTriliumFile
+```
\ No newline at end of file

diff --git a/README.md b/README.md
index f623b41..fefccb6 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,58 @@
 TriliumScripts
 ===============
 
-My collection of scripts for use with trilium
\ No newline at end of file
+Collection of scripts for use with trilium
+
+## syntax highlighting
+
+**highlight_moodlog.js** & **highlight_todo.js** - Both of these will apply appropriate syntaxy highlighting to the specific note.
+
+### installation
+
+1) add the code as the notes contents.
+
+2) set "note type" to "JS frontend"
+
+3) add an "owned attribute" of "#widget"
+
+### use
+
+on the note you want highlighting set the "owned attributes" to "#todotxt #hideHighlightWidget" or "#moodlog #hideHighlightWidget" depending on which syntax type you want to highlight
+
+## bidirectional sync
+
+**trilium_sync.php** - script to keep a folder of local text files and a trilium note in sync.
+
+### installation
+
+Edit the config values at the top of the script to match your trilium setup. Run it manually to test it is working as expected. If it is create a cron job.
+
+### example use
+
+```
+$> php trilium_sync.php 
+[i] checking connections
+[i] version: x.xx.x
+[i] gathering info
+
+- Folders -
+Folder/Path              Folder Name              Parent       noteId       Mod Trl    Mod Dsk    Status         
+Synced/                  Synced                   root         j0DRZhIzV97W 1742502936 1742507367 Exists
+Synced/BothPlaces/       BothPlaces               j0DRZhIzV97W AQb3Dg0yZHSu 1742507075 1742507200 Exists
+Synced/[del] tobe del/   [del] tobe del           j0DRZhIzV97W j0xUs1FdZL1Y 1742507321            Missing_Disk   
+Synced/AddToTrilium/     AddToTrilium                                                  1742507383 Missing_Trilium
+Synced/tobe deleted/     tobe deleted                                                  1742507200 Missing_Trilium
+- Files -
+Folder/Path              Filename                 Parent       noteId       Mod Trl    Mod Dsk    Hash  Status         
+Synced/BothPlaces/       UpdateDiskFile           AQb3Dg0yZHSu IwUZLwWdbiWG 1742507314 1742507199 Diff  Update_Disk    
+Synced/BothPlaces/       UpdateTriliumFile        AQb3Dg0yZHSu 7pMPgsb9AVRf 1742507130 1742507345 Diff  Update_Trilium 
+Synced/[del] tobe del/   gone_also.txt            j0xUs1FdZL1Y 4wOoHDy07csr 1742507178            N/A   Missing_Disk   
+Synced/AddToTrilium/     fromDiskToTrilium.txt                                         1742507394 N/A   Missing_Trilium
+Synced/tobe deleted/     gone_also.txt                                                 1742507200 N/A   Missing_Trilium
+
+[-] deleting disk folder: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/tobe deleted/
+[+] creating trilium folder: Synced/AddToTrilium/
+[+] creating trilium note: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/AddToTrilium/fromDiskToTrilium.txt
+[+] updating file on disk: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/BothPlaces/UpdateDiskFile
+[+] updating file on trilium: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/BothPlaces/UpdateTriliumFile
+```
\ No newline at end of file
diff --git a/highlight_moodlog.js b/highlight_moodlog.js
new file mode 100644
index 0000000..be4c283
--- /dev/null
+++ b/highlight_moodlog.js
@@ -0,0 +1,165 @@
+const TPL = `<div style="padding: 10px; border-top: 1px solid var(--main-border-color); contain: none;">
+    <strong>Moodlog Highlighting Applied</strong>
+</div>`;
+
+class MoodlogHighlighterWidget extends api.NoteContextAwareWidget {
+    static get parentWidget() { 
+        //console.log("MDLG: Getting parent widget: center-pane");
+        return 'center-pane'; 
+    }
+
+    get position() { 
+        //console.log("MDLG: Setting position: 100");
+        return 100; 
+    }
+
+    isEnabled() {
+        const enabled = super.isEnabled() 
+            && this.note.type === 'text' 
+            && this.note.hasLabel('moodlog');
+        //console.log(`MDLG: isEnabled: ${enabled}`);
+        return enabled;
+    }
+
+    doRender() {
+        //console.log("MDLG: Rendering widget...");
+        this.$widget = $(TPL);
+        //console.log("MDLG: Widget rendered:", this.$widget);
+        return this.$widget;
+    }
+
+    async refreshWithNote(note) {
+        //console.log("MDLG: Refreshing with note:", note);
+        const { content } = await note.getNoteComplement();
+        //console.log("MDLG: Note content fetched:", content);
+
+        // Check if content is fetched properly
+        if (!content) {
+            //console.log("MDLG: No content fetched. Exiting refresh.");
+            return;
+        }
+
+        const highlightedContent = this.applyHighlighting(content);
+        //console.log("MDLG: Highlighted content:", highlightedContent);
+
+        if (highlightedContent !== content) {
+            //console.log("MDLG: Updating note content with highlighted version");
+
+            // Use the setNoteContent function to update the note content
+            try {
+                await setNoteContent(note.noteId, highlightedContent);
+                //console.log("MDLG: Content updated with highlighted version");
+            } catch (error) {
+                //console.error("MDLG: Error updating content:", error);
+            }
+        } else {
+            //console.log("MDLG: No changes to content, skipping update.");
+        }
+    }
+
+
+ applyHighlighting(content) {
+    //console.log("MDLG: Applying highlighting to content...");
+
+    // Replace <br> with \n, then remove all other HTML tags
+    const plainTextContent = content.replace(/<br\s*\/?>/gi, '\n').replace(/<[^>]*>/g, ' ');
+    //console.log("MDLG: Stripped content:", plainTextContent);
+
+    // Combined regex for date & time
+    const dateTimePattern = /([1-2]\d{3}[-/.](0?[1-9]|1[0-2])[-/.](0[1-9]|[12]\d|3[01]))\s+(\d{1,2}:\d{2})/g;
+    const projectPattern = /\[\S+?\]/g;
+    const contextPattern = /(\s|^)(@\S+)/g;
+    const hashtagPattern = /(\s|^)(#\S+)/g;
+    const positivePattern = /(\s|^)(\+\S+)/g;
+    const negativePattern = /(\s|^)(-\S+)/g;
+
+    // Escape HTML characters to prevent issues with replacements
+    function escapeHtml(str) {
+        return str.replace(/[&<>"']/g, function (char) {
+            return {
+                '&': '&amp;',
+                '<': '&lt;',
+                '>': '&gt;',
+                '"': '&quot;',
+                "'": '&#039;'
+            }[char];
+        });
+    }
+
+      // Helper function to map the number to a colour based on the gradient
+    function getProjectColour(number) {
+        const minColor = { r: 107, g: 109, b: 229 };  // #6b6de5
+        const maxColor = { r: 70, g: 226, b: 96 };   // #46e260
+
+        const ratio = number / 9; // since we are working with 0-9
+
+        // Interpolate the RGB values between minColor and maxColor
+        const r = Math.round(minColor.r + ratio * (maxColor.r - minColor.r));
+        const g = Math.round(minColor.g + ratio * (maxColor.g - minColor.g));
+        const b = Math.round(minColor.b + ratio * (maxColor.b - minColor.b));
+
+        return `rgb(${r}, ${g}, ${b})`;
+    }
+     
+    // Process each line of the plain text content
+    let highlightedLines = plainTextContent.split("\n").map(line => {
+        //console.log("MDLG: Processing line:", line);
+
+        // Apply combined date & time highlight
+        line = line.replace(dateTimePattern, `<span style="color: rgb(188, 55, 237);">$1</span> <span style="color: rgb(188, 55, 237);">$4</span>`);
+
+        // Apply other highlights
+        line = line.replace(projectPattern, (match) => {
+            // Extract the number inside square brackets
+            const number = parseInt(match.match(/\d+/)[0], 10);
+            const colour = getProjectColour(number);
+            return `<span style="color: ${colour}; font-weight: bold;">${escapeHtml(match)}</span>`;
+        });
+        
+        // Apply other highlights
+        //line = line.replace(projectPattern, `<span style="color: rgb(224, 218, 36); font-weight: bold;">$&</span>`);
+        line = line.replace(contextPattern, (_, space, tag) => `${space}<span style="color: rgb(255 165 10);">${escapeHtml(tag)}</span>`);
+        line = line.replace(hashtagPattern, (_, space, tag) => `${space}<span style="color: rgb(41, 229, 242);">${escapeHtml(tag)}</span>`);
+        line = line.replace(positivePattern, (_, space, tag) => `${space}<span style="color: rgb(44, 227, 34);">${escapeHtml(tag)}</span>`);
+        line = line.replace(negativePattern, (_, space, tag) => `${space}<span style="color: rgb(230, 59, 44);">${escapeHtml(tag)}</span>`);
+
+        return line.trim();
+    });
+
+    // Join lines with proper <br> handling
+    let finalContent = highlightedLines.join("<br>");
+
+    //console.log("MDLG: Highlighted content:", finalContent);
+    return finalContent;
+}
+
+
+
+
+    async entitiesReloadedEvent({ loadResults }) {
+        //console.log("MDLG: Entities reloaded event triggered");
+        if (loadResults.isNoteContentReloaded(this.noteId)) {
+            //console.log("MDLG: Note content reloaded, refreshing widget...");
+            //this.refresh();
+        } else {
+            //console.log("MDLG: No content reload detected.");
+        }
+    }
+    
+
+}
+
+console.log("MDLG: Starting Moodlog Highlighter Widget");
+module.exports = MoodlogHighlighterWidget;
+
+function setNoteContent(noteId, content) {
+    /*const noteElement = document.querySelector(`note-detail-readonly-text-content`);
+
+    if (noteElement) {
+        noteElement.innerHTML = content; // Update the displayed content
+        console.log(`MDLG: Updated note ${noteId} in the DOM.`);
+    } else {
+        console.warn(`MDLG: Note element with data-note-id='${noteId}' not found.`);
+    }*/
+    return api.runOnBackend((id, data) => api.getNote(id).setContent(data), [noteId, content]);
+}

diff --git a/README.md b/README.md
index f623b41..fefccb6 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,58 @@
 TriliumScripts
 ===============
 
-My collection of scripts for use with trilium
\ No newline at end of file
+Collection of scripts for use with trilium
+
+## syntax highlighting
+
+**highlight_moodlog.js** & **highlight_todo.js** - Both of these will apply appropriate syntaxy highlighting to the specific note.
+
+### installation
+
+1) add the code as the notes contents.
+
+2) set "note type" to "JS frontend"
+
+3) add an "owned attribute" of "#widget"
+
+### use
+
+on the note you want highlighting set the "owned attributes" to "#todotxt #hideHighlightWidget" or "#moodlog #hideHighlightWidget" depending on which syntax type you want to highlight
+
+## bidirectional sync
+
+**trilium_sync.php** - script to keep a folder of local text files and a trilium note in sync.
+
+### installation
+
+Edit the config values at the top of the script to match your trilium setup. Run it manually to test it is working as expected. If it is create a cron job.
+
+### example use
+
+```
+$> php trilium_sync.php 
+[i] checking connections
+[i] version: x.xx.x
+[i] gathering info
+
+- Folders -
+Folder/Path              Folder Name              Parent       noteId       Mod Trl    Mod Dsk    Status         
+Synced/                  Synced                   root         j0DRZhIzV97W 1742502936 1742507367 Exists
+Synced/BothPlaces/       BothPlaces               j0DRZhIzV97W AQb3Dg0yZHSu 1742507075 1742507200 Exists
+Synced/[del] tobe del/   [del] tobe del           j0DRZhIzV97W j0xUs1FdZL1Y 1742507321            Missing_Disk   
+Synced/AddToTrilium/     AddToTrilium                                                  1742507383 Missing_Trilium
+Synced/tobe deleted/     tobe deleted                                                  1742507200 Missing_Trilium
+- Files -
+Folder/Path              Filename                 Parent       noteId       Mod Trl    Mod Dsk    Hash  Status         
+Synced/BothPlaces/       UpdateDiskFile           AQb3Dg0yZHSu IwUZLwWdbiWG 1742507314 1742507199 Diff  Update_Disk    
+Synced/BothPlaces/       UpdateTriliumFile        AQb3Dg0yZHSu 7pMPgsb9AVRf 1742507130 1742507345 Diff  Update_Trilium 
+Synced/[del] tobe del/   gone_also.txt            j0xUs1FdZL1Y 4wOoHDy07csr 1742507178            N/A   Missing_Disk   
+Synced/AddToTrilium/     fromDiskToTrilium.txt                                         1742507394 N/A   Missing_Trilium
+Synced/tobe deleted/     gone_also.txt                                                 1742507200 N/A   Missing_Trilium
+
+[-] deleting disk folder: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/tobe deleted/
+[+] creating trilium folder: Synced/AddToTrilium/
+[+] creating trilium note: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/AddToTrilium/fromDiskToTrilium.txt
+[+] updating file on disk: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/BothPlaces/UpdateDiskFile
+[+] updating file on trilium: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/BothPlaces/UpdateTriliumFile
+```
\ No newline at end of file
diff --git a/highlight_moodlog.js b/highlight_moodlog.js
new file mode 100644
index 0000000..be4c283
--- /dev/null
+++ b/highlight_moodlog.js
@@ -0,0 +1,165 @@
+const TPL = `<div style="padding: 10px; border-top: 1px solid var(--main-border-color); contain: none;">
+    <strong>Moodlog Highlighting Applied</strong>
+</div>`;
+
+class MoodlogHighlighterWidget extends api.NoteContextAwareWidget {
+    static get parentWidget() { 
+        //console.log("MDLG: Getting parent widget: center-pane");
+        return 'center-pane'; 
+    }
+
+    get position() { 
+        //console.log("MDLG: Setting position: 100");
+        return 100; 
+    }
+
+    isEnabled() {
+        const enabled = super.isEnabled() 
+            && this.note.type === 'text' 
+            && this.note.hasLabel('moodlog');
+        //console.log(`MDLG: isEnabled: ${enabled}`);
+        return enabled;
+    }
+
+    doRender() {
+        //console.log("MDLG: Rendering widget...");
+        this.$widget = $(TPL);
+        //console.log("MDLG: Widget rendered:", this.$widget);
+        return this.$widget;
+    }
+
+    async refreshWithNote(note) {
+        //console.log("MDLG: Refreshing with note:", note);
+        const { content } = await note.getNoteComplement();
+        //console.log("MDLG: Note content fetched:", content);
+
+        // Check if content is fetched properly
+        if (!content) {
+            //console.log("MDLG: No content fetched. Exiting refresh.");
+            return;
+        }
+
+        const highlightedContent = this.applyHighlighting(content);
+        //console.log("MDLG: Highlighted content:", highlightedContent);
+
+        if (highlightedContent !== content) {
+            //console.log("MDLG: Updating note content with highlighted version");
+
+            // Use the setNoteContent function to update the note content
+            try {
+                await setNoteContent(note.noteId, highlightedContent);
+                //console.log("MDLG: Content updated with highlighted version");
+            } catch (error) {
+                //console.error("MDLG: Error updating content:", error);
+            }
+        } else {
+            //console.log("MDLG: No changes to content, skipping update.");
+        }
+    }
+
+
+ applyHighlighting(content) {
+    //console.log("MDLG: Applying highlighting to content...");
+
+    // Replace <br> with \n, then remove all other HTML tags
+    const plainTextContent = content.replace(/<br\s*\/?>/gi, '\n').replace(/<[^>]*>/g, ' ');
+    //console.log("MDLG: Stripped content:", plainTextContent);
+
+    // Combined regex for date & time
+    const dateTimePattern = /([1-2]\d{3}[-/.](0?[1-9]|1[0-2])[-/.](0[1-9]|[12]\d|3[01]))\s+(\d{1,2}:\d{2})/g;
+    const projectPattern = /\[\S+?\]/g;
+    const contextPattern = /(\s|^)(@\S+)/g;
+    const hashtagPattern = /(\s|^)(#\S+)/g;
+    const positivePattern = /(\s|^)(\+\S+)/g;
+    const negativePattern = /(\s|^)(-\S+)/g;
+
+    // Escape HTML characters to prevent issues with replacements
+    function escapeHtml(str) {
+        return str.replace(/[&<>"']/g, function (char) {
+            return {
+                '&': '&amp;',
+                '<': '&lt;',
+                '>': '&gt;',
+                '"': '&quot;',
+                "'": '&#039;'
+            }[char];
+        });
+    }
+
+      // Helper function to map the number to a colour based on the gradient
+    function getProjectColour(number) {
+        const minColor = { r: 107, g: 109, b: 229 };  // #6b6de5
+        const maxColor = { r: 70, g: 226, b: 96 };   // #46e260
+
+        const ratio = number / 9; // since we are working with 0-9
+
+        // Interpolate the RGB values between minColor and maxColor
+        const r = Math.round(minColor.r + ratio * (maxColor.r - minColor.r));
+        const g = Math.round(minColor.g + ratio * (maxColor.g - minColor.g));
+        const b = Math.round(minColor.b + ratio * (maxColor.b - minColor.b));
+
+        return `rgb(${r}, ${g}, ${b})`;
+    }
+     
+    // Process each line of the plain text content
+    let highlightedLines = plainTextContent.split("\n").map(line => {
+        //console.log("MDLG: Processing line:", line);
+
+        // Apply combined date & time highlight
+        line = line.replace(dateTimePattern, `<span style="color: rgb(188, 55, 237);">$1</span> <span style="color: rgb(188, 55, 237);">$4</span>`);
+
+        // Apply other highlights
+        line = line.replace(projectPattern, (match) => {
+            // Extract the number inside square brackets
+            const number = parseInt(match.match(/\d+/)[0], 10);
+            const colour = getProjectColour(number);
+            return `<span style="color: ${colour}; font-weight: bold;">${escapeHtml(match)}</span>`;
+        });
+        
+        // Apply other highlights
+        //line = line.replace(projectPattern, `<span style="color: rgb(224, 218, 36); font-weight: bold;">$&</span>`);
+        line = line.replace(contextPattern, (_, space, tag) => `${space}<span style="color: rgb(255 165 10);">${escapeHtml(tag)}</span>`);
+        line = line.replace(hashtagPattern, (_, space, tag) => `${space}<span style="color: rgb(41, 229, 242);">${escapeHtml(tag)}</span>`);
+        line = line.replace(positivePattern, (_, space, tag) => `${space}<span style="color: rgb(44, 227, 34);">${escapeHtml(tag)}</span>`);
+        line = line.replace(negativePattern, (_, space, tag) => `${space}<span style="color: rgb(230, 59, 44);">${escapeHtml(tag)}</span>`);
+
+        return line.trim();
+    });
+
+    // Join lines with proper <br> handling
+    let finalContent = highlightedLines.join("<br>");
+
+    //console.log("MDLG: Highlighted content:", finalContent);
+    return finalContent;
+}
+
+
+
+
+    async entitiesReloadedEvent({ loadResults }) {
+        //console.log("MDLG: Entities reloaded event triggered");
+        if (loadResults.isNoteContentReloaded(this.noteId)) {
+            //console.log("MDLG: Note content reloaded, refreshing widget...");
+            //this.refresh();
+        } else {
+            //console.log("MDLG: No content reload detected.");
+        }
+    }
+    
+
+}
+
+console.log("MDLG: Starting Moodlog Highlighter Widget");
+module.exports = MoodlogHighlighterWidget;
+
+function setNoteContent(noteId, content) {
+    /*const noteElement = document.querySelector(`note-detail-readonly-text-content`);
+
+    if (noteElement) {
+        noteElement.innerHTML = content; // Update the displayed content
+        console.log(`MDLG: Updated note ${noteId} in the DOM.`);
+    } else {
+        console.warn(`MDLG: Note element with data-note-id='${noteId}' not found.`);
+    }*/
+    return api.runOnBackend((id, data) => api.getNote(id).setContent(data), [noteId, content]);
+}
diff --git a/highlight_todotxt.js b/highlight_todotxt.js
new file mode 100644
index 0000000..1f1b007
--- /dev/null
+++ b/highlight_todotxt.js
@@ -0,0 +1,157 @@
+const TPL = `<div style="padding: 10px; border-top: 1px solid var(--main-border-color); contain: none;">
+    <strong>Todo Highlighting Applied</strong>
+</div>`;
+
+class MoodlogHighlighterWidget extends api.NoteContextAwareWidget {
+    static get parentWidget() { 
+        //console.log("MDLG: Getting parent widget: center-pane");
+        return 'center-pane'; 
+    }
+
+    get position() { 
+        //console.log("MDLG: Setting position: 100");
+        return 100; 
+    }
+
+    isEnabled() {
+        const enabled = super.isEnabled() 
+            && this.note.type === 'text' 
+            && this.note.hasLabel('todotxt');
+        //console.log(`MDLG: isEnabled: ${enabled}`);
+        return enabled;
+    }
+
+    doRender() {
+        //console.log("MDLG: Rendering widget...");
+        this.$widget = $(TPL);
+        //console.log("MDLG: Widget rendered:", this.$widget);
+        return this.$widget;
+    }
+
+    async refreshWithNote(note) {
+        //console.log("MDLG: Refreshing with note:", note);
+        const { content } = await note.getNoteComplement();
+        //console.log("MDLG: Note content fetched:", content);
+
+        // Check if content is fetched properly
+        if (!content) {
+            //console.log("MDLG: No content fetched. Exiting refresh.");
+            return;
+        }
+
+        const highlightedContent = this.applyHighlighting(content);
+        //console.log("MDLG: Highlighted content:", highlightedContent);
+
+        if (highlightedContent !== content) {
+            //console.log("MDLG: Updating note content with highlighted version");
+
+            // Use the setNoteContent function to update the note content
+            try {
+                await setNoteContent(note.noteId, highlightedContent);
+                //console.log("MDLG: Content updated with highlighted version");
+            } catch (error) {
+                //console.error("MDLG: Error updating content:", error);
+            }
+        } else {
+            //console.log("MDLG: No changes to content, skipping update.");
+        }
+    }
+
+
+applyHighlighting(content) {
+    // Replace <br> with \n, then remove all other HTML tags
+    const plainTextContent = content.replace(/<br\s*\/?\>/gi, '\n').replace(/<[^>]*>/g, ' ');
+
+    // Regular expressions for Todo.txt syntax
+    const datePattern = /\b(0[1-9]|[12]\d|3[01])\/(0[1-9]|1[0-2])\/([1-2]\d{3})\b/g;
+    const completedTaskPattern = /^\s*x\s+.*/g;
+    const priorityPattern = /^(\s*\([A-E]\))\s+/;
+    const projectPattern = /(?:\s|^)(\+\S+)/g;
+    const contextPattern = /(?:\s|^)(@\S+)/g;
+    const waitPattern = /\bWAIT\b/gi;
+    const customAttributePattern = /(\s+[^\s:]+:[^\s:]+)+\s*$/g;
+    
+    function escapeHtml(str) {
+        return str.replace(/[&<>"']/g, char => ({
+            '&': '&amp;', '<': '&lt;', '>': '&gt;',
+            '"': '&quot;', "'": '&#039;'
+        })[char]);
+    }
+
+    let highlightedLines = plainTextContent.split("\n").map(line => {
+        // If line starts with 'x', make the whole line grey
+        if (/^\s*x\s+/.test(line)) {
+            return `<span style="color: grey;">${line}</span>`;
+        }
+        
+        // Highlight priority with different colours based on level
+        line = line.replace(priorityPattern, (match, priority) => {
+            const level = priority.match(/[A-E]/)?.[0];
+            const colors = {
+                'A': 'rgb(255, 0, 0)',       // Red
+                'B': 'rgb(255, 102, 0)',     // Orange
+                'C': 'rgb(255, 204, 0)',     // Yellow
+                'D': 'rgb(0, 153, 255)',     // Blue
+                'E': 'rgb(102, 204, 102)'    // Green
+            };
+            const color = colors[level] || 'black';
+            return `<span style="color: ${color}; font-weight: bold;">${escapeHtml(priority)}</span> `;
+        });
+        
+        // Highlight date
+        line = line.replace(datePattern, (match) => {
+            return `<span style="color: rgb(188, 55, 237);">${escapeHtml(match)}</span>`;
+        });
+
+        // Highlight project tags
+        line = line.replace(projectPattern, (_, tag) => `<span style="color: rgb(224, 218, 36);"> ${escapeHtml(tag)}</span>`);
+        
+        // Highlight context tags
+        line = line.replace(contextPattern, (_, tag) => `<span style="color: rgb(255, 165, 10);"> ${escapeHtml(tag)}</span>`);
+        
+        // Highlight WAIT command
+        line = line.replace(waitPattern, `<span style="color: rgb(255, 69, 0);">WAIT</span>`);
+        
+        // Highlight custom attributes
+        line = line.replace(customAttributePattern, `<span style="color: rgb(100, 149, 237);">$1</span>`);
+        
+        // Remove leading space (if any) from the beginning of the line, without affecting spaces before + or @
+        line = line.replace(/^\s+/, '');
+        
+        return line.trim();
+    });
+
+    return highlightedLines.join("<br>");
+}
+
+
+
+
+
+    async entitiesReloadedEvent({ loadResults }) {
+        //console.log("MDLG: Entities reloaded event triggered");
+        if (loadResults.isNoteContentReloaded(this.noteId)) {
+            //console.log("MDLG: Note content reloaded, refreshing widget...");
+            //this.refresh();
+        } else {
+            //console.log("MDLG: No content reload detected.");
+        }
+    }
+    
+
+}
+
+console.log("TodoHighlight: Starting todotxt Highlighter Widget");
+module.exports = MoodlogHighlighterWidget;
+
+function setNoteContent(noteId, content) {
+    /*const noteElement = document.querySelector(`note-detail-readonly-text-content`);
+
+    if (noteElement) {
+        noteElement.innerHTML = content; // Update the displayed content
+        console.log(`MDLG: Updated note ${noteId} in the DOM.`);
+    } else {
+        console.warn(`MDLG: Note element with data-note-id='${noteId}' not found.`);
+    }*/
+    return api.runOnBackend((id, data) => api.getNote(id).setContent(data), [noteId, content]);
+}

diff --git a/README.md b/README.md
index f623b41..fefccb6 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,58 @@
 TriliumScripts
 ===============
 
-My collection of scripts for use with trilium
\ No newline at end of file
+Collection of scripts for use with trilium
+
+## syntax highlighting
+
+**highlight_moodlog.js** & **highlight_todo.js** - Both of these will apply appropriate syntaxy highlighting to the specific note.
+
+### installation
+
+1) add the code as the notes contents.
+
+2) set "note type" to "JS frontend"
+
+3) add an "owned attribute" of "#widget"
+
+### use
+
+on the note you want highlighting set the "owned attributes" to "#todotxt #hideHighlightWidget" or "#moodlog #hideHighlightWidget" depending on which syntax type you want to highlight
+
+## bidirectional sync
+
+**trilium_sync.php** - script to keep a folder of local text files and a trilium note in sync.
+
+### installation
+
+Edit the config values at the top of the script to match your trilium setup. Run it manually to test it is working as expected. If it is create a cron job.
+
+### example use
+
+```
+$> php trilium_sync.php 
+[i] checking connections
+[i] version: x.xx.x
+[i] gathering info
+
+- Folders -
+Folder/Path              Folder Name              Parent       noteId       Mod Trl    Mod Dsk    Status         
+Synced/                  Synced                   root         j0DRZhIzV97W 1742502936 1742507367 Exists
+Synced/BothPlaces/       BothPlaces               j0DRZhIzV97W AQb3Dg0yZHSu 1742507075 1742507200 Exists
+Synced/[del] tobe del/   [del] tobe del           j0DRZhIzV97W j0xUs1FdZL1Y 1742507321            Missing_Disk   
+Synced/AddToTrilium/     AddToTrilium                                                  1742507383 Missing_Trilium
+Synced/tobe deleted/     tobe deleted                                                  1742507200 Missing_Trilium
+- Files -
+Folder/Path              Filename                 Parent       noteId       Mod Trl    Mod Dsk    Hash  Status         
+Synced/BothPlaces/       UpdateDiskFile           AQb3Dg0yZHSu IwUZLwWdbiWG 1742507314 1742507199 Diff  Update_Disk    
+Synced/BothPlaces/       UpdateTriliumFile        AQb3Dg0yZHSu 7pMPgsb9AVRf 1742507130 1742507345 Diff  Update_Trilium 
+Synced/[del] tobe del/   gone_also.txt            j0xUs1FdZL1Y 4wOoHDy07csr 1742507178            N/A   Missing_Disk   
+Synced/AddToTrilium/     fromDiskToTrilium.txt                                         1742507394 N/A   Missing_Trilium
+Synced/tobe deleted/     gone_also.txt                                                 1742507200 N/A   Missing_Trilium
+
+[-] deleting disk folder: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/tobe deleted/
+[+] creating trilium folder: Synced/AddToTrilium/
+[+] creating trilium note: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/AddToTrilium/fromDiskToTrilium.txt
+[+] updating file on disk: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/BothPlaces/UpdateDiskFile
+[+] updating file on trilium: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/BothPlaces/UpdateTriliumFile
+```
\ No newline at end of file
diff --git a/highlight_moodlog.js b/highlight_moodlog.js
new file mode 100644
index 0000000..be4c283
--- /dev/null
+++ b/highlight_moodlog.js
@@ -0,0 +1,165 @@
+const TPL = `<div style="padding: 10px; border-top: 1px solid var(--main-border-color); contain: none;">
+    <strong>Moodlog Highlighting Applied</strong>
+</div>`;
+
+class MoodlogHighlighterWidget extends api.NoteContextAwareWidget {
+    static get parentWidget() { 
+        //console.log("MDLG: Getting parent widget: center-pane");
+        return 'center-pane'; 
+    }
+
+    get position() { 
+        //console.log("MDLG: Setting position: 100");
+        return 100; 
+    }
+
+    isEnabled() {
+        const enabled = super.isEnabled() 
+            && this.note.type === 'text' 
+            && this.note.hasLabel('moodlog');
+        //console.log(`MDLG: isEnabled: ${enabled}`);
+        return enabled;
+    }
+
+    doRender() {
+        //console.log("MDLG: Rendering widget...");
+        this.$widget = $(TPL);
+        //console.log("MDLG: Widget rendered:", this.$widget);
+        return this.$widget;
+    }
+
+    async refreshWithNote(note) {
+        //console.log("MDLG: Refreshing with note:", note);
+        const { content } = await note.getNoteComplement();
+        //console.log("MDLG: Note content fetched:", content);
+
+        // Check if content is fetched properly
+        if (!content) {
+            //console.log("MDLG: No content fetched. Exiting refresh.");
+            return;
+        }
+
+        const highlightedContent = this.applyHighlighting(content);
+        //console.log("MDLG: Highlighted content:", highlightedContent);
+
+        if (highlightedContent !== content) {
+            //console.log("MDLG: Updating note content with highlighted version");
+
+            // Use the setNoteContent function to update the note content
+            try {
+                await setNoteContent(note.noteId, highlightedContent);
+                //console.log("MDLG: Content updated with highlighted version");
+            } catch (error) {
+                //console.error("MDLG: Error updating content:", error);
+            }
+        } else {
+            //console.log("MDLG: No changes to content, skipping update.");
+        }
+    }
+
+
+ applyHighlighting(content) {
+    //console.log("MDLG: Applying highlighting to content...");
+
+    // Replace <br> with \n, then remove all other HTML tags
+    const plainTextContent = content.replace(/<br\s*\/?>/gi, '\n').replace(/<[^>]*>/g, ' ');
+    //console.log("MDLG: Stripped content:", plainTextContent);
+
+    // Combined regex for date & time
+    const dateTimePattern = /([1-2]\d{3}[-/.](0?[1-9]|1[0-2])[-/.](0[1-9]|[12]\d|3[01]))\s+(\d{1,2}:\d{2})/g;
+    const projectPattern = /\[\S+?\]/g;
+    const contextPattern = /(\s|^)(@\S+)/g;
+    const hashtagPattern = /(\s|^)(#\S+)/g;
+    const positivePattern = /(\s|^)(\+\S+)/g;
+    const negativePattern = /(\s|^)(-\S+)/g;
+
+    // Escape HTML characters to prevent issues with replacements
+    function escapeHtml(str) {
+        return str.replace(/[&<>"']/g, function (char) {
+            return {
+                '&': '&amp;',
+                '<': '&lt;',
+                '>': '&gt;',
+                '"': '&quot;',
+                "'": '&#039;'
+            }[char];
+        });
+    }
+
+      // Helper function to map the number to a colour based on the gradient
+    function getProjectColour(number) {
+        const minColor = { r: 107, g: 109, b: 229 };  // #6b6de5
+        const maxColor = { r: 70, g: 226, b: 96 };   // #46e260
+
+        const ratio = number / 9; // since we are working with 0-9
+
+        // Interpolate the RGB values between minColor and maxColor
+        const r = Math.round(minColor.r + ratio * (maxColor.r - minColor.r));
+        const g = Math.round(minColor.g + ratio * (maxColor.g - minColor.g));
+        const b = Math.round(minColor.b + ratio * (maxColor.b - minColor.b));
+
+        return `rgb(${r}, ${g}, ${b})`;
+    }
+     
+    // Process each line of the plain text content
+    let highlightedLines = plainTextContent.split("\n").map(line => {
+        //console.log("MDLG: Processing line:", line);
+
+        // Apply combined date & time highlight
+        line = line.replace(dateTimePattern, `<span style="color: rgb(188, 55, 237);">$1</span> <span style="color: rgb(188, 55, 237);">$4</span>`);
+
+        // Apply other highlights
+        line = line.replace(projectPattern, (match) => {
+            // Extract the number inside square brackets
+            const number = parseInt(match.match(/\d+/)[0], 10);
+            const colour = getProjectColour(number);
+            return `<span style="color: ${colour}; font-weight: bold;">${escapeHtml(match)}</span>`;
+        });
+        
+        // Apply other highlights
+        //line = line.replace(projectPattern, `<span style="color: rgb(224, 218, 36); font-weight: bold;">$&</span>`);
+        line = line.replace(contextPattern, (_, space, tag) => `${space}<span style="color: rgb(255 165 10);">${escapeHtml(tag)}</span>`);
+        line = line.replace(hashtagPattern, (_, space, tag) => `${space}<span style="color: rgb(41, 229, 242);">${escapeHtml(tag)}</span>`);
+        line = line.replace(positivePattern, (_, space, tag) => `${space}<span style="color: rgb(44, 227, 34);">${escapeHtml(tag)}</span>`);
+        line = line.replace(negativePattern, (_, space, tag) => `${space}<span style="color: rgb(230, 59, 44);">${escapeHtml(tag)}</span>`);
+
+        return line.trim();
+    });
+
+    // Join lines with proper <br> handling
+    let finalContent = highlightedLines.join("<br>");
+
+    //console.log("MDLG: Highlighted content:", finalContent);
+    return finalContent;
+}
+
+
+
+
+    async entitiesReloadedEvent({ loadResults }) {
+        //console.log("MDLG: Entities reloaded event triggered");
+        if (loadResults.isNoteContentReloaded(this.noteId)) {
+            //console.log("MDLG: Note content reloaded, refreshing widget...");
+            //this.refresh();
+        } else {
+            //console.log("MDLG: No content reload detected.");
+        }
+    }
+    
+
+}
+
+console.log("MDLG: Starting Moodlog Highlighter Widget");
+module.exports = MoodlogHighlighterWidget;
+
+function setNoteContent(noteId, content) {
+    /*const noteElement = document.querySelector(`note-detail-readonly-text-content`);
+
+    if (noteElement) {
+        noteElement.innerHTML = content; // Update the displayed content
+        console.log(`MDLG: Updated note ${noteId} in the DOM.`);
+    } else {
+        console.warn(`MDLG: Note element with data-note-id='${noteId}' not found.`);
+    }*/
+    return api.runOnBackend((id, data) => api.getNote(id).setContent(data), [noteId, content]);
+}
diff --git a/highlight_todotxt.js b/highlight_todotxt.js
new file mode 100644
index 0000000..1f1b007
--- /dev/null
+++ b/highlight_todotxt.js
@@ -0,0 +1,157 @@
+const TPL = `<div style="padding: 10px; border-top: 1px solid var(--main-border-color); contain: none;">
+    <strong>Todo Highlighting Applied</strong>
+</div>`;
+
+class MoodlogHighlighterWidget extends api.NoteContextAwareWidget {
+    static get parentWidget() { 
+        //console.log("MDLG: Getting parent widget: center-pane");
+        return 'center-pane'; 
+    }
+
+    get position() { 
+        //console.log("MDLG: Setting position: 100");
+        return 100; 
+    }
+
+    isEnabled() {
+        const enabled = super.isEnabled() 
+            && this.note.type === 'text' 
+            && this.note.hasLabel('todotxt');
+        //console.log(`MDLG: isEnabled: ${enabled}`);
+        return enabled;
+    }
+
+    doRender() {
+        //console.log("MDLG: Rendering widget...");
+        this.$widget = $(TPL);
+        //console.log("MDLG: Widget rendered:", this.$widget);
+        return this.$widget;
+    }
+
+    async refreshWithNote(note) {
+        //console.log("MDLG: Refreshing with note:", note);
+        const { content } = await note.getNoteComplement();
+        //console.log("MDLG: Note content fetched:", content);
+
+        // Check if content is fetched properly
+        if (!content) {
+            //console.log("MDLG: No content fetched. Exiting refresh.");
+            return;
+        }
+
+        const highlightedContent = this.applyHighlighting(content);
+        //console.log("MDLG: Highlighted content:", highlightedContent);
+
+        if (highlightedContent !== content) {
+            //console.log("MDLG: Updating note content with highlighted version");
+
+            // Use the setNoteContent function to update the note content
+            try {
+                await setNoteContent(note.noteId, highlightedContent);
+                //console.log("MDLG: Content updated with highlighted version");
+            } catch (error) {
+                //console.error("MDLG: Error updating content:", error);
+            }
+        } else {
+            //console.log("MDLG: No changes to content, skipping update.");
+        }
+    }
+
+
+applyHighlighting(content) {
+    // Replace <br> with \n, then remove all other HTML tags
+    const plainTextContent = content.replace(/<br\s*\/?\>/gi, '\n').replace(/<[^>]*>/g, ' ');
+
+    // Regular expressions for Todo.txt syntax
+    const datePattern = /\b(0[1-9]|[12]\d|3[01])\/(0[1-9]|1[0-2])\/([1-2]\d{3})\b/g;
+    const completedTaskPattern = /^\s*x\s+.*/g;
+    const priorityPattern = /^(\s*\([A-E]\))\s+/;
+    const projectPattern = /(?:\s|^)(\+\S+)/g;
+    const contextPattern = /(?:\s|^)(@\S+)/g;
+    const waitPattern = /\bWAIT\b/gi;
+    const customAttributePattern = /(\s+[^\s:]+:[^\s:]+)+\s*$/g;
+    
+    function escapeHtml(str) {
+        return str.replace(/[&<>"']/g, char => ({
+            '&': '&amp;', '<': '&lt;', '>': '&gt;',
+            '"': '&quot;', "'": '&#039;'
+        })[char]);
+    }
+
+    let highlightedLines = plainTextContent.split("\n").map(line => {
+        // If line starts with 'x', make the whole line grey
+        if (/^\s*x\s+/.test(line)) {
+            return `<span style="color: grey;">${line}</span>`;
+        }
+        
+        // Highlight priority with different colours based on level
+        line = line.replace(priorityPattern, (match, priority) => {
+            const level = priority.match(/[A-E]/)?.[0];
+            const colors = {
+                'A': 'rgb(255, 0, 0)',       // Red
+                'B': 'rgb(255, 102, 0)',     // Orange
+                'C': 'rgb(255, 204, 0)',     // Yellow
+                'D': 'rgb(0, 153, 255)',     // Blue
+                'E': 'rgb(102, 204, 102)'    // Green
+            };
+            const color = colors[level] || 'black';
+            return `<span style="color: ${color}; font-weight: bold;">${escapeHtml(priority)}</span> `;
+        });
+        
+        // Highlight date
+        line = line.replace(datePattern, (match) => {
+            return `<span style="color: rgb(188, 55, 237);">${escapeHtml(match)}</span>`;
+        });
+
+        // Highlight project tags
+        line = line.replace(projectPattern, (_, tag) => `<span style="color: rgb(224, 218, 36);"> ${escapeHtml(tag)}</span>`);
+        
+        // Highlight context tags
+        line = line.replace(contextPattern, (_, tag) => `<span style="color: rgb(255, 165, 10);"> ${escapeHtml(tag)}</span>`);
+        
+        // Highlight WAIT command
+        line = line.replace(waitPattern, `<span style="color: rgb(255, 69, 0);">WAIT</span>`);
+        
+        // Highlight custom attributes
+        line = line.replace(customAttributePattern, `<span style="color: rgb(100, 149, 237);">$1</span>`);
+        
+        // Remove leading space (if any) from the beginning of the line, without affecting spaces before + or @
+        line = line.replace(/^\s+/, '');
+        
+        return line.trim();
+    });
+
+    return highlightedLines.join("<br>");
+}
+
+
+
+
+
+    async entitiesReloadedEvent({ loadResults }) {
+        //console.log("MDLG: Entities reloaded event triggered");
+        if (loadResults.isNoteContentReloaded(this.noteId)) {
+            //console.log("MDLG: Note content reloaded, refreshing widget...");
+            //this.refresh();
+        } else {
+            //console.log("MDLG: No content reload detected.");
+        }
+    }
+    
+
+}
+
+console.log("TodoHighlight: Starting todotxt Highlighter Widget");
+module.exports = MoodlogHighlighterWidget;
+
+function setNoteContent(noteId, content) {
+    /*const noteElement = document.querySelector(`note-detail-readonly-text-content`);
+
+    if (noteElement) {
+        noteElement.innerHTML = content; // Update the displayed content
+        console.log(`MDLG: Updated note ${noteId} in the DOM.`);
+    } else {
+        console.warn(`MDLG: Note element with data-note-id='${noteId}' not found.`);
+    }*/
+    return api.runOnBackend((id, data) => api.getNote(id).setContent(data), [noteId, content]);
+}
diff --git a/htb2trilium/INSTALL.md b/htb2trilium/INSTALL.md
new file mode 100644
index 0000000..da8afad
--- /dev/null
+++ b/htb2trilium/INSTALL.md
@@ -0,0 +1,41 @@
+## scripts require:
+
+```
+ > pip3 install trilium-py
+```
+
+## config.json
+- get trilium token in trilium options->ETAPI and "create new token"
+- create a new note with a title "Machines"
+- click on this note -> "note info". Note ID is there. This ID goes in "trilium_machines_htb_folder" in the config file
+- set the folder's owned attributes to:
+
+```
+#label:user=promoted,single,text #label:root=promoted,single,text #label:respect=promoted,single,text #user=0 #root=0 #respect=0 
+```
+
+ - create a note named "HTBMachineTemplate" 
+ - set it's "owned attributes" to:
+
+```
+#template #label:User=promoted,single,text #label:Root=promoted,single,text #label:Tags=promoted,single,text
+```
+
+- get this page's ID and put in "trilium_machines_template_id"
+- create "challenges" page, this page's ID goes in: trilium_challenges_folder  
+- create "Sherlocks" page, this page's ID goes in: trilium_sherlocks_folder  
+- create 2 pages "HTBChallengesTemplate" and "HTBSherlocksTemplate", both of these should have the owned attributes:
+
+```
+#template #label:Difficulty=promoted,single,text #label:Solved=promoted,single,text #label:Released=promoted,single,text 
+```
+
+- The ID's of "HTBChallengesTemplate" and "HTBSherlocksTemplate" go in the matching config values.
+
+- to get "in progress" todo colour - add to trilium demo/scripting/taskmanager/implementation/CSS:
+
+```
+ span.fancytree-node.inprogress .fancytree-title {
+    color: orange !important;
+ }
+```
\ No newline at end of file

diff --git a/README.md b/README.md
index f623b41..fefccb6 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,58 @@
 TriliumScripts
 ===============
 
-My collection of scripts for use with trilium
\ No newline at end of file
+Collection of scripts for use with trilium
+
+## syntax highlighting
+
+**highlight_moodlog.js** & **highlight_todo.js** - Both of these will apply appropriate syntaxy highlighting to the specific note.
+
+### installation
+
+1) add the code as the notes contents.
+
+2) set "note type" to "JS frontend"
+
+3) add an "owned attribute" of "#widget"
+
+### use
+
+on the note you want highlighting set the "owned attributes" to "#todotxt #hideHighlightWidget" or "#moodlog #hideHighlightWidget" depending on which syntax type you want to highlight
+
+## bidirectional sync
+
+**trilium_sync.php** - script to keep a folder of local text files and a trilium note in sync.
+
+### installation
+
+Edit the config values at the top of the script to match your trilium setup. Run it manually to test it is working as expected. If it is create a cron job.
+
+### example use
+
+```
+$> php trilium_sync.php 
+[i] checking connections
+[i] version: x.xx.x
+[i] gathering info
+
+- Folders -
+Folder/Path              Folder Name              Parent       noteId       Mod Trl    Mod Dsk    Status         
+Synced/                  Synced                   root         j0DRZhIzV97W 1742502936 1742507367 Exists
+Synced/BothPlaces/       BothPlaces               j0DRZhIzV97W AQb3Dg0yZHSu 1742507075 1742507200 Exists
+Synced/[del] tobe del/   [del] tobe del           j0DRZhIzV97W j0xUs1FdZL1Y 1742507321            Missing_Disk   
+Synced/AddToTrilium/     AddToTrilium                                                  1742507383 Missing_Trilium
+Synced/tobe deleted/     tobe deleted                                                  1742507200 Missing_Trilium
+- Files -
+Folder/Path              Filename                 Parent       noteId       Mod Trl    Mod Dsk    Hash  Status         
+Synced/BothPlaces/       UpdateDiskFile           AQb3Dg0yZHSu IwUZLwWdbiWG 1742507314 1742507199 Diff  Update_Disk    
+Synced/BothPlaces/       UpdateTriliumFile        AQb3Dg0yZHSu 7pMPgsb9AVRf 1742507130 1742507345 Diff  Update_Trilium 
+Synced/[del] tobe del/   gone_also.txt            j0xUs1FdZL1Y 4wOoHDy07csr 1742507178            N/A   Missing_Disk   
+Synced/AddToTrilium/     fromDiskToTrilium.txt                                         1742507394 N/A   Missing_Trilium
+Synced/tobe deleted/     gone_also.txt                                                 1742507200 N/A   Missing_Trilium
+
+[-] deleting disk folder: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/tobe deleted/
+[+] creating trilium folder: Synced/AddToTrilium/
+[+] creating trilium note: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/AddToTrilium/fromDiskToTrilium.txt
+[+] updating file on disk: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/BothPlaces/UpdateDiskFile
+[+] updating file on trilium: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/BothPlaces/UpdateTriliumFile
+```
\ No newline at end of file
diff --git a/highlight_moodlog.js b/highlight_moodlog.js
new file mode 100644
index 0000000..be4c283
--- /dev/null
+++ b/highlight_moodlog.js
@@ -0,0 +1,165 @@
+const TPL = `<div style="padding: 10px; border-top: 1px solid var(--main-border-color); contain: none;">
+    <strong>Moodlog Highlighting Applied</strong>
+</div>`;
+
+class MoodlogHighlighterWidget extends api.NoteContextAwareWidget {
+    static get parentWidget() { 
+        //console.log("MDLG: Getting parent widget: center-pane");
+        return 'center-pane'; 
+    }
+
+    get position() { 
+        //console.log("MDLG: Setting position: 100");
+        return 100; 
+    }
+
+    isEnabled() {
+        const enabled = super.isEnabled() 
+            && this.note.type === 'text' 
+            && this.note.hasLabel('moodlog');
+        //console.log(`MDLG: isEnabled: ${enabled}`);
+        return enabled;
+    }
+
+    doRender() {
+        //console.log("MDLG: Rendering widget...");
+        this.$widget = $(TPL);
+        //console.log("MDLG: Widget rendered:", this.$widget);
+        return this.$widget;
+    }
+
+    async refreshWithNote(note) {
+        //console.log("MDLG: Refreshing with note:", note);
+        const { content } = await note.getNoteComplement();
+        //console.log("MDLG: Note content fetched:", content);
+
+        // Check if content is fetched properly
+        if (!content) {
+            //console.log("MDLG: No content fetched. Exiting refresh.");
+            return;
+        }
+
+        const highlightedContent = this.applyHighlighting(content);
+        //console.log("MDLG: Highlighted content:", highlightedContent);
+
+        if (highlightedContent !== content) {
+            //console.log("MDLG: Updating note content with highlighted version");
+
+            // Use the setNoteContent function to update the note content
+            try {
+                await setNoteContent(note.noteId, highlightedContent);
+                //console.log("MDLG: Content updated with highlighted version");
+            } catch (error) {
+                //console.error("MDLG: Error updating content:", error);
+            }
+        } else {
+            //console.log("MDLG: No changes to content, skipping update.");
+        }
+    }
+
+
+ applyHighlighting(content) {
+    //console.log("MDLG: Applying highlighting to content...");
+
+    // Replace <br> with \n, then remove all other HTML tags
+    const plainTextContent = content.replace(/<br\s*\/?>/gi, '\n').replace(/<[^>]*>/g, ' ');
+    //console.log("MDLG: Stripped content:", plainTextContent);
+
+    // Combined regex for date & time
+    const dateTimePattern = /([1-2]\d{3}[-/.](0?[1-9]|1[0-2])[-/.](0[1-9]|[12]\d|3[01]))\s+(\d{1,2}:\d{2})/g;
+    const projectPattern = /\[\S+?\]/g;
+    const contextPattern = /(\s|^)(@\S+)/g;
+    const hashtagPattern = /(\s|^)(#\S+)/g;
+    const positivePattern = /(\s|^)(\+\S+)/g;
+    const negativePattern = /(\s|^)(-\S+)/g;
+
+    // Escape HTML characters to prevent issues with replacements
+    function escapeHtml(str) {
+        return str.replace(/[&<>"']/g, function (char) {
+            return {
+                '&': '&amp;',
+                '<': '&lt;',
+                '>': '&gt;',
+                '"': '&quot;',
+                "'": '&#039;'
+            }[char];
+        });
+    }
+
+      // Helper function to map the number to a colour based on the gradient
+    function getProjectColour(number) {
+        const minColor = { r: 107, g: 109, b: 229 };  // #6b6de5
+        const maxColor = { r: 70, g: 226, b: 96 };   // #46e260
+
+        const ratio = number / 9; // since we are working with 0-9
+
+        // Interpolate the RGB values between minColor and maxColor
+        const r = Math.round(minColor.r + ratio * (maxColor.r - minColor.r));
+        const g = Math.round(minColor.g + ratio * (maxColor.g - minColor.g));
+        const b = Math.round(minColor.b + ratio * (maxColor.b - minColor.b));
+
+        return `rgb(${r}, ${g}, ${b})`;
+    }
+     
+    // Process each line of the plain text content
+    let highlightedLines = plainTextContent.split("\n").map(line => {
+        //console.log("MDLG: Processing line:", line);
+
+        // Apply combined date & time highlight
+        line = line.replace(dateTimePattern, `<span style="color: rgb(188, 55, 237);">$1</span> <span style="color: rgb(188, 55, 237);">$4</span>`);
+
+        // Apply other highlights
+        line = line.replace(projectPattern, (match) => {
+            // Extract the number inside square brackets
+            const number = parseInt(match.match(/\d+/)[0], 10);
+            const colour = getProjectColour(number);
+            return `<span style="color: ${colour}; font-weight: bold;">${escapeHtml(match)}</span>`;
+        });
+        
+        // Apply other highlights
+        //line = line.replace(projectPattern, `<span style="color: rgb(224, 218, 36); font-weight: bold;">$&</span>`);
+        line = line.replace(contextPattern, (_, space, tag) => `${space}<span style="color: rgb(255 165 10);">${escapeHtml(tag)}</span>`);
+        line = line.replace(hashtagPattern, (_, space, tag) => `${space}<span style="color: rgb(41, 229, 242);">${escapeHtml(tag)}</span>`);
+        line = line.replace(positivePattern, (_, space, tag) => `${space}<span style="color: rgb(44, 227, 34);">${escapeHtml(tag)}</span>`);
+        line = line.replace(negativePattern, (_, space, tag) => `${space}<span style="color: rgb(230, 59, 44);">${escapeHtml(tag)}</span>`);
+
+        return line.trim();
+    });
+
+    // Join lines with proper <br> handling
+    let finalContent = highlightedLines.join("<br>");
+
+    //console.log("MDLG: Highlighted content:", finalContent);
+    return finalContent;
+}
+
+
+
+
+    async entitiesReloadedEvent({ loadResults }) {
+        //console.log("MDLG: Entities reloaded event triggered");
+        if (loadResults.isNoteContentReloaded(this.noteId)) {
+            //console.log("MDLG: Note content reloaded, refreshing widget...");
+            //this.refresh();
+        } else {
+            //console.log("MDLG: No content reload detected.");
+        }
+    }
+    
+
+}
+
+console.log("MDLG: Starting Moodlog Highlighter Widget");
+module.exports = MoodlogHighlighterWidget;
+
+function setNoteContent(noteId, content) {
+    /*const noteElement = document.querySelector(`note-detail-readonly-text-content`);
+
+    if (noteElement) {
+        noteElement.innerHTML = content; // Update the displayed content
+        console.log(`MDLG: Updated note ${noteId} in the DOM.`);
+    } else {
+        console.warn(`MDLG: Note element with data-note-id='${noteId}' not found.`);
+    }*/
+    return api.runOnBackend((id, data) => api.getNote(id).setContent(data), [noteId, content]);
+}
diff --git a/highlight_todotxt.js b/highlight_todotxt.js
new file mode 100644
index 0000000..1f1b007
--- /dev/null
+++ b/highlight_todotxt.js
@@ -0,0 +1,157 @@
+const TPL = `<div style="padding: 10px; border-top: 1px solid var(--main-border-color); contain: none;">
+    <strong>Todo Highlighting Applied</strong>
+</div>`;
+
+class MoodlogHighlighterWidget extends api.NoteContextAwareWidget {
+    static get parentWidget() { 
+        //console.log("MDLG: Getting parent widget: center-pane");
+        return 'center-pane'; 
+    }
+
+    get position() { 
+        //console.log("MDLG: Setting position: 100");
+        return 100; 
+    }
+
+    isEnabled() {
+        const enabled = super.isEnabled() 
+            && this.note.type === 'text' 
+            && this.note.hasLabel('todotxt');
+        //console.log(`MDLG: isEnabled: ${enabled}`);
+        return enabled;
+    }
+
+    doRender() {
+        //console.log("MDLG: Rendering widget...");
+        this.$widget = $(TPL);
+        //console.log("MDLG: Widget rendered:", this.$widget);
+        return this.$widget;
+    }
+
+    async refreshWithNote(note) {
+        //console.log("MDLG: Refreshing with note:", note);
+        const { content } = await note.getNoteComplement();
+        //console.log("MDLG: Note content fetched:", content);
+
+        // Check if content is fetched properly
+        if (!content) {
+            //console.log("MDLG: No content fetched. Exiting refresh.");
+            return;
+        }
+
+        const highlightedContent = this.applyHighlighting(content);
+        //console.log("MDLG: Highlighted content:", highlightedContent);
+
+        if (highlightedContent !== content) {
+            //console.log("MDLG: Updating note content with highlighted version");
+
+            // Use the setNoteContent function to update the note content
+            try {
+                await setNoteContent(note.noteId, highlightedContent);
+                //console.log("MDLG: Content updated with highlighted version");
+            } catch (error) {
+                //console.error("MDLG: Error updating content:", error);
+            }
+        } else {
+            //console.log("MDLG: No changes to content, skipping update.");
+        }
+    }
+
+
+applyHighlighting(content) {
+    // Replace <br> with \n, then remove all other HTML tags
+    const plainTextContent = content.replace(/<br\s*\/?\>/gi, '\n').replace(/<[^>]*>/g, ' ');
+
+    // Regular expressions for Todo.txt syntax
+    const datePattern = /\b(0[1-9]|[12]\d|3[01])\/(0[1-9]|1[0-2])\/([1-2]\d{3})\b/g;
+    const completedTaskPattern = /^\s*x\s+.*/g;
+    const priorityPattern = /^(\s*\([A-E]\))\s+/;
+    const projectPattern = /(?:\s|^)(\+\S+)/g;
+    const contextPattern = /(?:\s|^)(@\S+)/g;
+    const waitPattern = /\bWAIT\b/gi;
+    const customAttributePattern = /(\s+[^\s:]+:[^\s:]+)+\s*$/g;
+    
+    function escapeHtml(str) {
+        return str.replace(/[&<>"']/g, char => ({
+            '&': '&amp;', '<': '&lt;', '>': '&gt;',
+            '"': '&quot;', "'": '&#039;'
+        })[char]);
+    }
+
+    let highlightedLines = plainTextContent.split("\n").map(line => {
+        // If line starts with 'x', make the whole line grey
+        if (/^\s*x\s+/.test(line)) {
+            return `<span style="color: grey;">${line}</span>`;
+        }
+        
+        // Highlight priority with different colours based on level
+        line = line.replace(priorityPattern, (match, priority) => {
+            const level = priority.match(/[A-E]/)?.[0];
+            const colors = {
+                'A': 'rgb(255, 0, 0)',       // Red
+                'B': 'rgb(255, 102, 0)',     // Orange
+                'C': 'rgb(255, 204, 0)',     // Yellow
+                'D': 'rgb(0, 153, 255)',     // Blue
+                'E': 'rgb(102, 204, 102)'    // Green
+            };
+            const color = colors[level] || 'black';
+            return `<span style="color: ${color}; font-weight: bold;">${escapeHtml(priority)}</span> `;
+        });
+        
+        // Highlight date
+        line = line.replace(datePattern, (match) => {
+            return `<span style="color: rgb(188, 55, 237);">${escapeHtml(match)}</span>`;
+        });
+
+        // Highlight project tags
+        line = line.replace(projectPattern, (_, tag) => `<span style="color: rgb(224, 218, 36);"> ${escapeHtml(tag)}</span>`);
+        
+        // Highlight context tags
+        line = line.replace(contextPattern, (_, tag) => `<span style="color: rgb(255, 165, 10);"> ${escapeHtml(tag)}</span>`);
+        
+        // Highlight WAIT command
+        line = line.replace(waitPattern, `<span style="color: rgb(255, 69, 0);">WAIT</span>`);
+        
+        // Highlight custom attributes
+        line = line.replace(customAttributePattern, `<span style="color: rgb(100, 149, 237);">$1</span>`);
+        
+        // Remove leading space (if any) from the beginning of the line, without affecting spaces before + or @
+        line = line.replace(/^\s+/, '');
+        
+        return line.trim();
+    });
+
+    return highlightedLines.join("<br>");
+}
+
+
+
+
+
+    async entitiesReloadedEvent({ loadResults }) {
+        //console.log("MDLG: Entities reloaded event triggered");
+        if (loadResults.isNoteContentReloaded(this.noteId)) {
+            //console.log("MDLG: Note content reloaded, refreshing widget...");
+            //this.refresh();
+        } else {
+            //console.log("MDLG: No content reload detected.");
+        }
+    }
+    
+
+}
+
+console.log("TodoHighlight: Starting todotxt Highlighter Widget");
+module.exports = MoodlogHighlighterWidget;
+
+function setNoteContent(noteId, content) {
+    /*const noteElement = document.querySelector(`note-detail-readonly-text-content`);
+
+    if (noteElement) {
+        noteElement.innerHTML = content; // Update the displayed content
+        console.log(`MDLG: Updated note ${noteId} in the DOM.`);
+    } else {
+        console.warn(`MDLG: Note element with data-note-id='${noteId}' not found.`);
+    }*/
+    return api.runOnBackend((id, data) => api.getNote(id).setContent(data), [noteId, content]);
+}
diff --git a/htb2trilium/INSTALL.md b/htb2trilium/INSTALL.md
new file mode 100644
index 0000000..da8afad
--- /dev/null
+++ b/htb2trilium/INSTALL.md
@@ -0,0 +1,41 @@
+## scripts require:
+
+```
+ > pip3 install trilium-py
+```
+
+## config.json
+- get trilium token in trilium options->ETAPI and "create new token"
+- create a new note with a title "Machines"
+- click on this note -> "note info". Note ID is there. This ID goes in "trilium_machines_htb_folder" in the config file
+- set the folder's owned attributes to:
+
+```
+#label:user=promoted,single,text #label:root=promoted,single,text #label:respect=promoted,single,text #user=0 #root=0 #respect=0 
+```
+
+ - create a note named "HTBMachineTemplate" 
+ - set it's "owned attributes" to:
+
+```
+#template #label:User=promoted,single,text #label:Root=promoted,single,text #label:Tags=promoted,single,text
+```
+
+- get this page's ID and put in "trilium_machines_template_id"
+- create "challenges" page, this page's ID goes in: trilium_challenges_folder  
+- create "Sherlocks" page, this page's ID goes in: trilium_sherlocks_folder  
+- create 2 pages "HTBChallengesTemplate" and "HTBSherlocksTemplate", both of these should have the owned attributes:
+
+```
+#template #label:Difficulty=promoted,single,text #label:Solved=promoted,single,text #label:Released=promoted,single,text 
+```
+
+- The ID's of "HTBChallengesTemplate" and "HTBSherlocksTemplate" go in the matching config values.
+
+- to get "in progress" todo colour - add to trilium demo/scripting/taskmanager/implementation/CSS:
+
+```
+ span.fancytree-node.inprogress .fancytree-title {
+    color: orange !important;
+ }
+```
\ No newline at end of file
diff --git a/htb2trilium/README.md b/htb2trilium/README.md
new file mode 100644
index 0000000..1b45ae2
--- /dev/null
+++ b/htb2trilium/README.md
@@ -0,0 +1,16 @@
+htb2trilium
+===============
+
+Pull hackthebox info and stats into personal trilium note taking software
+
+single machine view:
+
+![machine view](images/machine_view.png)
+
+overview of machines:
+
+![machines overview](images/machines_overview.png)
+
+navigation:
+
+![navigation](images/navigation.png)
\ No newline at end of file

diff --git a/README.md b/README.md
index f623b41..fefccb6 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,58 @@
 TriliumScripts
 ===============
 
-My collection of scripts for use with trilium
\ No newline at end of file
+Collection of scripts for use with trilium
+
+## syntax highlighting
+
+**highlight_moodlog.js** & **highlight_todo.js** - Both of these will apply appropriate syntaxy highlighting to the specific note.
+
+### installation
+
+1) add the code as the notes contents.
+
+2) set "note type" to "JS frontend"
+
+3) add an "owned attribute" of "#widget"
+
+### use
+
+on the note you want highlighting set the "owned attributes" to "#todotxt #hideHighlightWidget" or "#moodlog #hideHighlightWidget" depending on which syntax type you want to highlight
+
+## bidirectional sync
+
+**trilium_sync.php** - script to keep a folder of local text files and a trilium note in sync.
+
+### installation
+
+Edit the config values at the top of the script to match your trilium setup. Run it manually to test it is working as expected. If it is create a cron job.
+
+### example use
+
+```
+$> php trilium_sync.php 
+[i] checking connections
+[i] version: x.xx.x
+[i] gathering info
+
+- Folders -
+Folder/Path              Folder Name              Parent       noteId       Mod Trl    Mod Dsk    Status         
+Synced/                  Synced                   root         j0DRZhIzV97W 1742502936 1742507367 Exists
+Synced/BothPlaces/       BothPlaces               j0DRZhIzV97W AQb3Dg0yZHSu 1742507075 1742507200 Exists
+Synced/[del] tobe del/   [del] tobe del           j0DRZhIzV97W j0xUs1FdZL1Y 1742507321            Missing_Disk   
+Synced/AddToTrilium/     AddToTrilium                                                  1742507383 Missing_Trilium
+Synced/tobe deleted/     tobe deleted                                                  1742507200 Missing_Trilium
+- Files -
+Folder/Path              Filename                 Parent       noteId       Mod Trl    Mod Dsk    Hash  Status         
+Synced/BothPlaces/       UpdateDiskFile           AQb3Dg0yZHSu IwUZLwWdbiWG 1742507314 1742507199 Diff  Update_Disk    
+Synced/BothPlaces/       UpdateTriliumFile        AQb3Dg0yZHSu 7pMPgsb9AVRf 1742507130 1742507345 Diff  Update_Trilium 
+Synced/[del] tobe del/   gone_also.txt            j0xUs1FdZL1Y 4wOoHDy07csr 1742507178            N/A   Missing_Disk   
+Synced/AddToTrilium/     fromDiskToTrilium.txt                                         1742507394 N/A   Missing_Trilium
+Synced/tobe deleted/     gone_also.txt                                                 1742507200 N/A   Missing_Trilium
+
+[-] deleting disk folder: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/tobe deleted/
+[+] creating trilium folder: Synced/AddToTrilium/
+[+] creating trilium note: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/AddToTrilium/fromDiskToTrilium.txt
+[+] updating file on disk: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/BothPlaces/UpdateDiskFile
+[+] updating file on trilium: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/BothPlaces/UpdateTriliumFile
+```
\ No newline at end of file
diff --git a/highlight_moodlog.js b/highlight_moodlog.js
new file mode 100644
index 0000000..be4c283
--- /dev/null
+++ b/highlight_moodlog.js
@@ -0,0 +1,165 @@
+const TPL = `<div style="padding: 10px; border-top: 1px solid var(--main-border-color); contain: none;">
+    <strong>Moodlog Highlighting Applied</strong>
+</div>`;
+
+class MoodlogHighlighterWidget extends api.NoteContextAwareWidget {
+    static get parentWidget() { 
+        //console.log("MDLG: Getting parent widget: center-pane");
+        return 'center-pane'; 
+    }
+
+    get position() { 
+        //console.log("MDLG: Setting position: 100");
+        return 100; 
+    }
+
+    isEnabled() {
+        const enabled = super.isEnabled() 
+            && this.note.type === 'text' 
+            && this.note.hasLabel('moodlog');
+        //console.log(`MDLG: isEnabled: ${enabled}`);
+        return enabled;
+    }
+
+    doRender() {
+        //console.log("MDLG: Rendering widget...");
+        this.$widget = $(TPL);
+        //console.log("MDLG: Widget rendered:", this.$widget);
+        return this.$widget;
+    }
+
+    async refreshWithNote(note) {
+        //console.log("MDLG: Refreshing with note:", note);
+        const { content } = await note.getNoteComplement();
+        //console.log("MDLG: Note content fetched:", content);
+
+        // Check if content is fetched properly
+        if (!content) {
+            //console.log("MDLG: No content fetched. Exiting refresh.");
+            return;
+        }
+
+        const highlightedContent = this.applyHighlighting(content);
+        //console.log("MDLG: Highlighted content:", highlightedContent);
+
+        if (highlightedContent !== content) {
+            //console.log("MDLG: Updating note content with highlighted version");
+
+            // Use the setNoteContent function to update the note content
+            try {
+                await setNoteContent(note.noteId, highlightedContent);
+                //console.log("MDLG: Content updated with highlighted version");
+            } catch (error) {
+                //console.error("MDLG: Error updating content:", error);
+            }
+        } else {
+            //console.log("MDLG: No changes to content, skipping update.");
+        }
+    }
+
+
+ applyHighlighting(content) {
+    //console.log("MDLG: Applying highlighting to content...");
+
+    // Replace <br> with \n, then remove all other HTML tags
+    const plainTextContent = content.replace(/<br\s*\/?>/gi, '\n').replace(/<[^>]*>/g, ' ');
+    //console.log("MDLG: Stripped content:", plainTextContent);
+
+    // Combined regex for date & time
+    const dateTimePattern = /([1-2]\d{3}[-/.](0?[1-9]|1[0-2])[-/.](0[1-9]|[12]\d|3[01]))\s+(\d{1,2}:\d{2})/g;
+    const projectPattern = /\[\S+?\]/g;
+    const contextPattern = /(\s|^)(@\S+)/g;
+    const hashtagPattern = /(\s|^)(#\S+)/g;
+    const positivePattern = /(\s|^)(\+\S+)/g;
+    const negativePattern = /(\s|^)(-\S+)/g;
+
+    // Escape HTML characters to prevent issues with replacements
+    function escapeHtml(str) {
+        return str.replace(/[&<>"']/g, function (char) {
+            return {
+                '&': '&amp;',
+                '<': '&lt;',
+                '>': '&gt;',
+                '"': '&quot;',
+                "'": '&#039;'
+            }[char];
+        });
+    }
+
+      // Helper function to map the number to a colour based on the gradient
+    function getProjectColour(number) {
+        const minColor = { r: 107, g: 109, b: 229 };  // #6b6de5
+        const maxColor = { r: 70, g: 226, b: 96 };   // #46e260
+
+        const ratio = number / 9; // since we are working with 0-9
+
+        // Interpolate the RGB values between minColor and maxColor
+        const r = Math.round(minColor.r + ratio * (maxColor.r - minColor.r));
+        const g = Math.round(minColor.g + ratio * (maxColor.g - minColor.g));
+        const b = Math.round(minColor.b + ratio * (maxColor.b - minColor.b));
+
+        return `rgb(${r}, ${g}, ${b})`;
+    }
+     
+    // Process each line of the plain text content
+    let highlightedLines = plainTextContent.split("\n").map(line => {
+        //console.log("MDLG: Processing line:", line);
+
+        // Apply combined date & time highlight
+        line = line.replace(dateTimePattern, `<span style="color: rgb(188, 55, 237);">$1</span> <span style="color: rgb(188, 55, 237);">$4</span>`);
+
+        // Apply other highlights
+        line = line.replace(projectPattern, (match) => {
+            // Extract the number inside square brackets
+            const number = parseInt(match.match(/\d+/)[0], 10);
+            const colour = getProjectColour(number);
+            return `<span style="color: ${colour}; font-weight: bold;">${escapeHtml(match)}</span>`;
+        });
+        
+        // Apply other highlights
+        //line = line.replace(projectPattern, `<span style="color: rgb(224, 218, 36); font-weight: bold;">$&</span>`);
+        line = line.replace(contextPattern, (_, space, tag) => `${space}<span style="color: rgb(255 165 10);">${escapeHtml(tag)}</span>`);
+        line = line.replace(hashtagPattern, (_, space, tag) => `${space}<span style="color: rgb(41, 229, 242);">${escapeHtml(tag)}</span>`);
+        line = line.replace(positivePattern, (_, space, tag) => `${space}<span style="color: rgb(44, 227, 34);">${escapeHtml(tag)}</span>`);
+        line = line.replace(negativePattern, (_, space, tag) => `${space}<span style="color: rgb(230, 59, 44);">${escapeHtml(tag)}</span>`);
+
+        return line.trim();
+    });
+
+    // Join lines with proper <br> handling
+    let finalContent = highlightedLines.join("<br>");
+
+    //console.log("MDLG: Highlighted content:", finalContent);
+    return finalContent;
+}
+
+
+
+
+    async entitiesReloadedEvent({ loadResults }) {
+        //console.log("MDLG: Entities reloaded event triggered");
+        if (loadResults.isNoteContentReloaded(this.noteId)) {
+            //console.log("MDLG: Note content reloaded, refreshing widget...");
+            //this.refresh();
+        } else {
+            //console.log("MDLG: No content reload detected.");
+        }
+    }
+    
+
+}
+
+console.log("MDLG: Starting Moodlog Highlighter Widget");
+module.exports = MoodlogHighlighterWidget;
+
+function setNoteContent(noteId, content) {
+    /*const noteElement = document.querySelector(`note-detail-readonly-text-content`);
+
+    if (noteElement) {
+        noteElement.innerHTML = content; // Update the displayed content
+        console.log(`MDLG: Updated note ${noteId} in the DOM.`);
+    } else {
+        console.warn(`MDLG: Note element with data-note-id='${noteId}' not found.`);
+    }*/
+    return api.runOnBackend((id, data) => api.getNote(id).setContent(data), [noteId, content]);
+}
diff --git a/highlight_todotxt.js b/highlight_todotxt.js
new file mode 100644
index 0000000..1f1b007
--- /dev/null
+++ b/highlight_todotxt.js
@@ -0,0 +1,157 @@
+const TPL = `<div style="padding: 10px; border-top: 1px solid var(--main-border-color); contain: none;">
+    <strong>Todo Highlighting Applied</strong>
+</div>`;
+
+class MoodlogHighlighterWidget extends api.NoteContextAwareWidget {
+    static get parentWidget() { 
+        //console.log("MDLG: Getting parent widget: center-pane");
+        return 'center-pane'; 
+    }
+
+    get position() { 
+        //console.log("MDLG: Setting position: 100");
+        return 100; 
+    }
+
+    isEnabled() {
+        const enabled = super.isEnabled() 
+            && this.note.type === 'text' 
+            && this.note.hasLabel('todotxt');
+        //console.log(`MDLG: isEnabled: ${enabled}`);
+        return enabled;
+    }
+
+    doRender() {
+        //console.log("MDLG: Rendering widget...");
+        this.$widget = $(TPL);
+        //console.log("MDLG: Widget rendered:", this.$widget);
+        return this.$widget;
+    }
+
+    async refreshWithNote(note) {
+        //console.log("MDLG: Refreshing with note:", note);
+        const { content } = await note.getNoteComplement();
+        //console.log("MDLG: Note content fetched:", content);
+
+        // Check if content is fetched properly
+        if (!content) {
+            //console.log("MDLG: No content fetched. Exiting refresh.");
+            return;
+        }
+
+        const highlightedContent = this.applyHighlighting(content);
+        //console.log("MDLG: Highlighted content:", highlightedContent);
+
+        if (highlightedContent !== content) {
+            //console.log("MDLG: Updating note content with highlighted version");
+
+            // Use the setNoteContent function to update the note content
+            try {
+                await setNoteContent(note.noteId, highlightedContent);
+                //console.log("MDLG: Content updated with highlighted version");
+            } catch (error) {
+                //console.error("MDLG: Error updating content:", error);
+            }
+        } else {
+            //console.log("MDLG: No changes to content, skipping update.");
+        }
+    }
+
+
+applyHighlighting(content) {
+    // Replace <br> with \n, then remove all other HTML tags
+    const plainTextContent = content.replace(/<br\s*\/?\>/gi, '\n').replace(/<[^>]*>/g, ' ');
+
+    // Regular expressions for Todo.txt syntax
+    const datePattern = /\b(0[1-9]|[12]\d|3[01])\/(0[1-9]|1[0-2])\/([1-2]\d{3})\b/g;
+    const completedTaskPattern = /^\s*x\s+.*/g;
+    const priorityPattern = /^(\s*\([A-E]\))\s+/;
+    const projectPattern = /(?:\s|^)(\+\S+)/g;
+    const contextPattern = /(?:\s|^)(@\S+)/g;
+    const waitPattern = /\bWAIT\b/gi;
+    const customAttributePattern = /(\s+[^\s:]+:[^\s:]+)+\s*$/g;
+    
+    function escapeHtml(str) {
+        return str.replace(/[&<>"']/g, char => ({
+            '&': '&amp;', '<': '&lt;', '>': '&gt;',
+            '"': '&quot;', "'": '&#039;'
+        })[char]);
+    }
+
+    let highlightedLines = plainTextContent.split("\n").map(line => {
+        // If line starts with 'x', make the whole line grey
+        if (/^\s*x\s+/.test(line)) {
+            return `<span style="color: grey;">${line}</span>`;
+        }
+        
+        // Highlight priority with different colours based on level
+        line = line.replace(priorityPattern, (match, priority) => {
+            const level = priority.match(/[A-E]/)?.[0];
+            const colors = {
+                'A': 'rgb(255, 0, 0)',       // Red
+                'B': 'rgb(255, 102, 0)',     // Orange
+                'C': 'rgb(255, 204, 0)',     // Yellow
+                'D': 'rgb(0, 153, 255)',     // Blue
+                'E': 'rgb(102, 204, 102)'    // Green
+            };
+            const color = colors[level] || 'black';
+            return `<span style="color: ${color}; font-weight: bold;">${escapeHtml(priority)}</span> `;
+        });
+        
+        // Highlight date
+        line = line.replace(datePattern, (match) => {
+            return `<span style="color: rgb(188, 55, 237);">${escapeHtml(match)}</span>`;
+        });
+
+        // Highlight project tags
+        line = line.replace(projectPattern, (_, tag) => `<span style="color: rgb(224, 218, 36);"> ${escapeHtml(tag)}</span>`);
+        
+        // Highlight context tags
+        line = line.replace(contextPattern, (_, tag) => `<span style="color: rgb(255, 165, 10);"> ${escapeHtml(tag)}</span>`);
+        
+        // Highlight WAIT command
+        line = line.replace(waitPattern, `<span style="color: rgb(255, 69, 0);">WAIT</span>`);
+        
+        // Highlight custom attributes
+        line = line.replace(customAttributePattern, `<span style="color: rgb(100, 149, 237);">$1</span>`);
+        
+        // Remove leading space (if any) from the beginning of the line, without affecting spaces before + or @
+        line = line.replace(/^\s+/, '');
+        
+        return line.trim();
+    });
+
+    return highlightedLines.join("<br>");
+}
+
+
+
+
+
+    async entitiesReloadedEvent({ loadResults }) {
+        //console.log("MDLG: Entities reloaded event triggered");
+        if (loadResults.isNoteContentReloaded(this.noteId)) {
+            //console.log("MDLG: Note content reloaded, refreshing widget...");
+            //this.refresh();
+        } else {
+            //console.log("MDLG: No content reload detected.");
+        }
+    }
+    
+
+}
+
+console.log("TodoHighlight: Starting todotxt Highlighter Widget");
+module.exports = MoodlogHighlighterWidget;
+
+function setNoteContent(noteId, content) {
+    /*const noteElement = document.querySelector(`note-detail-readonly-text-content`);
+
+    if (noteElement) {
+        noteElement.innerHTML = content; // Update the displayed content
+        console.log(`MDLG: Updated note ${noteId} in the DOM.`);
+    } else {
+        console.warn(`MDLG: Note element with data-note-id='${noteId}' not found.`);
+    }*/
+    return api.runOnBackend((id, data) => api.getNote(id).setContent(data), [noteId, content]);
+}
diff --git a/htb2trilium/INSTALL.md b/htb2trilium/INSTALL.md
new file mode 100644
index 0000000..da8afad
--- /dev/null
+++ b/htb2trilium/INSTALL.md
@@ -0,0 +1,41 @@
+## scripts require:
+
+```
+ > pip3 install trilium-py
+```
+
+## config.json
+- get trilium token in trilium options->ETAPI and "create new token"
+- create a new note with a title "Machines"
+- click on this note -> "note info". Note ID is there. This ID goes in "trilium_machines_htb_folder" in the config file
+- set the folder's owned attributes to:
+
+```
+#label:user=promoted,single,text #label:root=promoted,single,text #label:respect=promoted,single,text #user=0 #root=0 #respect=0 
+```
+
+ - create a note named "HTBMachineTemplate" 
+ - set it's "owned attributes" to:
+
+```
+#template #label:User=promoted,single,text #label:Root=promoted,single,text #label:Tags=promoted,single,text
+```
+
+- get this page's ID and put in "trilium_machines_template_id"
+- create "challenges" page, this page's ID goes in: trilium_challenges_folder  
+- create "Sherlocks" page, this page's ID goes in: trilium_sherlocks_folder  
+- create 2 pages "HTBChallengesTemplate" and "HTBSherlocksTemplate", both of these should have the owned attributes:
+
+```
+#template #label:Difficulty=promoted,single,text #label:Solved=promoted,single,text #label:Released=promoted,single,text 
+```
+
+- The ID's of "HTBChallengesTemplate" and "HTBSherlocksTemplate" go in the matching config values.
+
+- to get "in progress" todo colour - add to trilium demo/scripting/taskmanager/implementation/CSS:
+
+```
+ span.fancytree-node.inprogress .fancytree-title {
+    color: orange !important;
+ }
+```
\ No newline at end of file
diff --git a/htb2trilium/README.md b/htb2trilium/README.md
new file mode 100644
index 0000000..1b45ae2
--- /dev/null
+++ b/htb2trilium/README.md
@@ -0,0 +1,16 @@
+htb2trilium
+===============
+
+Pull hackthebox info and stats into personal trilium note taking software
+
+single machine view:
+
+![machine view](images/machine_view.png)
+
+overview of machines:
+
+![machines overview](images/machines_overview.png)
+
+navigation:
+
+![navigation](images/navigation.png)
\ No newline at end of file
diff --git a/htb2trilium/config.json b/htb2trilium/config.json
new file mode 100644
index 0000000..39597ce
--- /dev/null
+++ b/htb2trilium/config.json
@@ -0,0 +1,11 @@
+{
+	"htb_code": "in https://app.hackthebox.com/profile/settings click 'profile settings' and under 'app tokens' you can 'create app token' - that goes here",
+	"trilium_server_url": "https://place.to.your.trilium.server",
+	"trilium_token": "",
+	"trilium_machines_htb_folder": "",
+	"trilium_machines_template_id": "",
+	"trilium_challenges_folder": "",
+	"trilium_challenges_template_id": "",
+	"trilium_sherlocks_folder": "",
+	"trilium_sherlocks_template_id": ""
+}
\ No newline at end of file

diff --git a/README.md b/README.md
index f623b41..fefccb6 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,58 @@
 TriliumScripts
 ===============
 
-My collection of scripts for use with trilium
\ No newline at end of file
+Collection of scripts for use with trilium
+
+## syntax highlighting
+
+**highlight_moodlog.js** & **highlight_todo.js** - Both of these will apply appropriate syntaxy highlighting to the specific note.
+
+### installation
+
+1) add the code as the notes contents.
+
+2) set "note type" to "JS frontend"
+
+3) add an "owned attribute" of "#widget"
+
+### use
+
+on the note you want highlighting set the "owned attributes" to "#todotxt #hideHighlightWidget" or "#moodlog #hideHighlightWidget" depending on which syntax type you want to highlight
+
+## bidirectional sync
+
+**trilium_sync.php** - script to keep a folder of local text files and a trilium note in sync.
+
+### installation
+
+Edit the config values at the top of the script to match your trilium setup. Run it manually to test it is working as expected. If it is create a cron job.
+
+### example use
+
+```
+$> php trilium_sync.php 
+[i] checking connections
+[i] version: x.xx.x
+[i] gathering info
+
+- Folders -
+Folder/Path              Folder Name              Parent       noteId       Mod Trl    Mod Dsk    Status         
+Synced/                  Synced                   root         j0DRZhIzV97W 1742502936 1742507367 Exists
+Synced/BothPlaces/       BothPlaces               j0DRZhIzV97W AQb3Dg0yZHSu 1742507075 1742507200 Exists
+Synced/[del] tobe del/   [del] tobe del           j0DRZhIzV97W j0xUs1FdZL1Y 1742507321            Missing_Disk   
+Synced/AddToTrilium/     AddToTrilium                                                  1742507383 Missing_Trilium
+Synced/tobe deleted/     tobe deleted                                                  1742507200 Missing_Trilium
+- Files -
+Folder/Path              Filename                 Parent       noteId       Mod Trl    Mod Dsk    Hash  Status         
+Synced/BothPlaces/       UpdateDiskFile           AQb3Dg0yZHSu IwUZLwWdbiWG 1742507314 1742507199 Diff  Update_Disk    
+Synced/BothPlaces/       UpdateTriliumFile        AQb3Dg0yZHSu 7pMPgsb9AVRf 1742507130 1742507345 Diff  Update_Trilium 
+Synced/[del] tobe del/   gone_also.txt            j0xUs1FdZL1Y 4wOoHDy07csr 1742507178            N/A   Missing_Disk   
+Synced/AddToTrilium/     fromDiskToTrilium.txt                                         1742507394 N/A   Missing_Trilium
+Synced/tobe deleted/     gone_also.txt                                                 1742507200 N/A   Missing_Trilium
+
+[-] deleting disk folder: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/tobe deleted/
+[+] creating trilium folder: Synced/AddToTrilium/
+[+] creating trilium note: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/AddToTrilium/fromDiskToTrilium.txt
+[+] updating file on disk: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/BothPlaces/UpdateDiskFile
+[+] updating file on trilium: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/BothPlaces/UpdateTriliumFile
+```
\ No newline at end of file
diff --git a/highlight_moodlog.js b/highlight_moodlog.js
new file mode 100644
index 0000000..be4c283
--- /dev/null
+++ b/highlight_moodlog.js
@@ -0,0 +1,165 @@
+const TPL = `<div style="padding: 10px; border-top: 1px solid var(--main-border-color); contain: none;">
+    <strong>Moodlog Highlighting Applied</strong>
+</div>`;
+
+class MoodlogHighlighterWidget extends api.NoteContextAwareWidget {
+    static get parentWidget() { 
+        //console.log("MDLG: Getting parent widget: center-pane");
+        return 'center-pane'; 
+    }
+
+    get position() { 
+        //console.log("MDLG: Setting position: 100");
+        return 100; 
+    }
+
+    isEnabled() {
+        const enabled = super.isEnabled() 
+            && this.note.type === 'text' 
+            && this.note.hasLabel('moodlog');
+        //console.log(`MDLG: isEnabled: ${enabled}`);
+        return enabled;
+    }
+
+    doRender() {
+        //console.log("MDLG: Rendering widget...");
+        this.$widget = $(TPL);
+        //console.log("MDLG: Widget rendered:", this.$widget);
+        return this.$widget;
+    }
+
+    async refreshWithNote(note) {
+        //console.log("MDLG: Refreshing with note:", note);
+        const { content } = await note.getNoteComplement();
+        //console.log("MDLG: Note content fetched:", content);
+
+        // Check if content is fetched properly
+        if (!content) {
+            //console.log("MDLG: No content fetched. Exiting refresh.");
+            return;
+        }
+
+        const highlightedContent = this.applyHighlighting(content);
+        //console.log("MDLG: Highlighted content:", highlightedContent);
+
+        if (highlightedContent !== content) {
+            //console.log("MDLG: Updating note content with highlighted version");
+
+            // Use the setNoteContent function to update the note content
+            try {
+                await setNoteContent(note.noteId, highlightedContent);
+                //console.log("MDLG: Content updated with highlighted version");
+            } catch (error) {
+                //console.error("MDLG: Error updating content:", error);
+            }
+        } else {
+            //console.log("MDLG: No changes to content, skipping update.");
+        }
+    }
+
+
+ applyHighlighting(content) {
+    //console.log("MDLG: Applying highlighting to content...");
+
+    // Replace <br> with \n, then remove all other HTML tags
+    const plainTextContent = content.replace(/<br\s*\/?>/gi, '\n').replace(/<[^>]*>/g, ' ');
+    //console.log("MDLG: Stripped content:", plainTextContent);
+
+    // Combined regex for date & time
+    const dateTimePattern = /([1-2]\d{3}[-/.](0?[1-9]|1[0-2])[-/.](0[1-9]|[12]\d|3[01]))\s+(\d{1,2}:\d{2})/g;
+    const projectPattern = /\[\S+?\]/g;
+    const contextPattern = /(\s|^)(@\S+)/g;
+    const hashtagPattern = /(\s|^)(#\S+)/g;
+    const positivePattern = /(\s|^)(\+\S+)/g;
+    const negativePattern = /(\s|^)(-\S+)/g;
+
+    // Escape HTML characters to prevent issues with replacements
+    function escapeHtml(str) {
+        return str.replace(/[&<>"']/g, function (char) {
+            return {
+                '&': '&amp;',
+                '<': '&lt;',
+                '>': '&gt;',
+                '"': '&quot;',
+                "'": '&#039;'
+            }[char];
+        });
+    }
+
+      // Helper function to map the number to a colour based on the gradient
+    function getProjectColour(number) {
+        const minColor = { r: 107, g: 109, b: 229 };  // #6b6de5
+        const maxColor = { r: 70, g: 226, b: 96 };   // #46e260
+
+        const ratio = number / 9; // since we are working with 0-9
+
+        // Interpolate the RGB values between minColor and maxColor
+        const r = Math.round(minColor.r + ratio * (maxColor.r - minColor.r));
+        const g = Math.round(minColor.g + ratio * (maxColor.g - minColor.g));
+        const b = Math.round(minColor.b + ratio * (maxColor.b - minColor.b));
+
+        return `rgb(${r}, ${g}, ${b})`;
+    }
+     
+    // Process each line of the plain text content
+    let highlightedLines = plainTextContent.split("\n").map(line => {
+        //console.log("MDLG: Processing line:", line);
+
+        // Apply combined date & time highlight
+        line = line.replace(dateTimePattern, `<span style="color: rgb(188, 55, 237);">$1</span> <span style="color: rgb(188, 55, 237);">$4</span>`);
+
+        // Apply other highlights
+        line = line.replace(projectPattern, (match) => {
+            // Extract the number inside square brackets
+            const number = parseInt(match.match(/\d+/)[0], 10);
+            const colour = getProjectColour(number);
+            return `<span style="color: ${colour}; font-weight: bold;">${escapeHtml(match)}</span>`;
+        });
+        
+        // Apply other highlights
+        //line = line.replace(projectPattern, `<span style="color: rgb(224, 218, 36); font-weight: bold;">$&</span>`);
+        line = line.replace(contextPattern, (_, space, tag) => `${space}<span style="color: rgb(255 165 10);">${escapeHtml(tag)}</span>`);
+        line = line.replace(hashtagPattern, (_, space, tag) => `${space}<span style="color: rgb(41, 229, 242);">${escapeHtml(tag)}</span>`);
+        line = line.replace(positivePattern, (_, space, tag) => `${space}<span style="color: rgb(44, 227, 34);">${escapeHtml(tag)}</span>`);
+        line = line.replace(negativePattern, (_, space, tag) => `${space}<span style="color: rgb(230, 59, 44);">${escapeHtml(tag)}</span>`);
+
+        return line.trim();
+    });
+
+    // Join lines with proper <br> handling
+    let finalContent = highlightedLines.join("<br>");
+
+    //console.log("MDLG: Highlighted content:", finalContent);
+    return finalContent;
+}
+
+
+
+
+    async entitiesReloadedEvent({ loadResults }) {
+        //console.log("MDLG: Entities reloaded event triggered");
+        if (loadResults.isNoteContentReloaded(this.noteId)) {
+            //console.log("MDLG: Note content reloaded, refreshing widget...");
+            //this.refresh();
+        } else {
+            //console.log("MDLG: No content reload detected.");
+        }
+    }
+    
+
+}
+
+console.log("MDLG: Starting Moodlog Highlighter Widget");
+module.exports = MoodlogHighlighterWidget;
+
+function setNoteContent(noteId, content) {
+    /*const noteElement = document.querySelector(`note-detail-readonly-text-content`);
+
+    if (noteElement) {
+        noteElement.innerHTML = content; // Update the displayed content
+        console.log(`MDLG: Updated note ${noteId} in the DOM.`);
+    } else {
+        console.warn(`MDLG: Note element with data-note-id='${noteId}' not found.`);
+    }*/
+    return api.runOnBackend((id, data) => api.getNote(id).setContent(data), [noteId, content]);
+}
diff --git a/highlight_todotxt.js b/highlight_todotxt.js
new file mode 100644
index 0000000..1f1b007
--- /dev/null
+++ b/highlight_todotxt.js
@@ -0,0 +1,157 @@
+const TPL = `<div style="padding: 10px; border-top: 1px solid var(--main-border-color); contain: none;">
+    <strong>Todo Highlighting Applied</strong>
+</div>`;
+
+class MoodlogHighlighterWidget extends api.NoteContextAwareWidget {
+    static get parentWidget() { 
+        //console.log("MDLG: Getting parent widget: center-pane");
+        return 'center-pane'; 
+    }
+
+    get position() { 
+        //console.log("MDLG: Setting position: 100");
+        return 100; 
+    }
+
+    isEnabled() {
+        const enabled = super.isEnabled() 
+            && this.note.type === 'text' 
+            && this.note.hasLabel('todotxt');
+        //console.log(`MDLG: isEnabled: ${enabled}`);
+        return enabled;
+    }
+
+    doRender() {
+        //console.log("MDLG: Rendering widget...");
+        this.$widget = $(TPL);
+        //console.log("MDLG: Widget rendered:", this.$widget);
+        return this.$widget;
+    }
+
+    async refreshWithNote(note) {
+        //console.log("MDLG: Refreshing with note:", note);
+        const { content } = await note.getNoteComplement();
+        //console.log("MDLG: Note content fetched:", content);
+
+        // Check if content is fetched properly
+        if (!content) {
+            //console.log("MDLG: No content fetched. Exiting refresh.");
+            return;
+        }
+
+        const highlightedContent = this.applyHighlighting(content);
+        //console.log("MDLG: Highlighted content:", highlightedContent);
+
+        if (highlightedContent !== content) {
+            //console.log("MDLG: Updating note content with highlighted version");
+
+            // Use the setNoteContent function to update the note content
+            try {
+                await setNoteContent(note.noteId, highlightedContent);
+                //console.log("MDLG: Content updated with highlighted version");
+            } catch (error) {
+                //console.error("MDLG: Error updating content:", error);
+            }
+        } else {
+            //console.log("MDLG: No changes to content, skipping update.");
+        }
+    }
+
+
+applyHighlighting(content) {
+    // Replace <br> with \n, then remove all other HTML tags
+    const plainTextContent = content.replace(/<br\s*\/?\>/gi, '\n').replace(/<[^>]*>/g, ' ');
+
+    // Regular expressions for Todo.txt syntax
+    const datePattern = /\b(0[1-9]|[12]\d|3[01])\/(0[1-9]|1[0-2])\/([1-2]\d{3})\b/g;
+    const completedTaskPattern = /^\s*x\s+.*/g;
+    const priorityPattern = /^(\s*\([A-E]\))\s+/;
+    const projectPattern = /(?:\s|^)(\+\S+)/g;
+    const contextPattern = /(?:\s|^)(@\S+)/g;
+    const waitPattern = /\bWAIT\b/gi;
+    const customAttributePattern = /(\s+[^\s:]+:[^\s:]+)+\s*$/g;
+    
+    function escapeHtml(str) {
+        return str.replace(/[&<>"']/g, char => ({
+            '&': '&amp;', '<': '&lt;', '>': '&gt;',
+            '"': '&quot;', "'": '&#039;'
+        })[char]);
+    }
+
+    let highlightedLines = plainTextContent.split("\n").map(line => {
+        // If line starts with 'x', make the whole line grey
+        if (/^\s*x\s+/.test(line)) {
+            return `<span style="color: grey;">${line}</span>`;
+        }
+        
+        // Highlight priority with different colours based on level
+        line = line.replace(priorityPattern, (match, priority) => {
+            const level = priority.match(/[A-E]/)?.[0];
+            const colors = {
+                'A': 'rgb(255, 0, 0)',       // Red
+                'B': 'rgb(255, 102, 0)',     // Orange
+                'C': 'rgb(255, 204, 0)',     // Yellow
+                'D': 'rgb(0, 153, 255)',     // Blue
+                'E': 'rgb(102, 204, 102)'    // Green
+            };
+            const color = colors[level] || 'black';
+            return `<span style="color: ${color}; font-weight: bold;">${escapeHtml(priority)}</span> `;
+        });
+        
+        // Highlight date
+        line = line.replace(datePattern, (match) => {
+            return `<span style="color: rgb(188, 55, 237);">${escapeHtml(match)}</span>`;
+        });
+
+        // Highlight project tags
+        line = line.replace(projectPattern, (_, tag) => `<span style="color: rgb(224, 218, 36);"> ${escapeHtml(tag)}</span>`);
+        
+        // Highlight context tags
+        line = line.replace(contextPattern, (_, tag) => `<span style="color: rgb(255, 165, 10);"> ${escapeHtml(tag)}</span>`);
+        
+        // Highlight WAIT command
+        line = line.replace(waitPattern, `<span style="color: rgb(255, 69, 0);">WAIT</span>`);
+        
+        // Highlight custom attributes
+        line = line.replace(customAttributePattern, `<span style="color: rgb(100, 149, 237);">$1</span>`);
+        
+        // Remove leading space (if any) from the beginning of the line, without affecting spaces before + or @
+        line = line.replace(/^\s+/, '');
+        
+        return line.trim();
+    });
+
+    return highlightedLines.join("<br>");
+}
+
+
+
+
+
+    async entitiesReloadedEvent({ loadResults }) {
+        //console.log("MDLG: Entities reloaded event triggered");
+        if (loadResults.isNoteContentReloaded(this.noteId)) {
+            //console.log("MDLG: Note content reloaded, refreshing widget...");
+            //this.refresh();
+        } else {
+            //console.log("MDLG: No content reload detected.");
+        }
+    }
+    
+
+}
+
+console.log("TodoHighlight: Starting todotxt Highlighter Widget");
+module.exports = MoodlogHighlighterWidget;
+
+function setNoteContent(noteId, content) {
+    /*const noteElement = document.querySelector(`note-detail-readonly-text-content`);
+
+    if (noteElement) {
+        noteElement.innerHTML = content; // Update the displayed content
+        console.log(`MDLG: Updated note ${noteId} in the DOM.`);
+    } else {
+        console.warn(`MDLG: Note element with data-note-id='${noteId}' not found.`);
+    }*/
+    return api.runOnBackend((id, data) => api.getNote(id).setContent(data), [noteId, content]);
+}
diff --git a/htb2trilium/INSTALL.md b/htb2trilium/INSTALL.md
new file mode 100644
index 0000000..da8afad
--- /dev/null
+++ b/htb2trilium/INSTALL.md
@@ -0,0 +1,41 @@
+## scripts require:
+
+```
+ > pip3 install trilium-py
+```
+
+## config.json
+- get trilium token in trilium options->ETAPI and "create new token"
+- create a new note with a title "Machines"
+- click on this note -> "note info". Note ID is there. This ID goes in "trilium_machines_htb_folder" in the config file
+- set the folder's owned attributes to:
+
+```
+#label:user=promoted,single,text #label:root=promoted,single,text #label:respect=promoted,single,text #user=0 #root=0 #respect=0 
+```
+
+ - create a note named "HTBMachineTemplate" 
+ - set it's "owned attributes" to:
+
+```
+#template #label:User=promoted,single,text #label:Root=promoted,single,text #label:Tags=promoted,single,text
+```
+
+- get this page's ID and put in "trilium_machines_template_id"
+- create "challenges" page, this page's ID goes in: trilium_challenges_folder  
+- create "Sherlocks" page, this page's ID goes in: trilium_sherlocks_folder  
+- create 2 pages "HTBChallengesTemplate" and "HTBSherlocksTemplate", both of these should have the owned attributes:
+
+```
+#template #label:Difficulty=promoted,single,text #label:Solved=promoted,single,text #label:Released=promoted,single,text 
+```
+
+- The ID's of "HTBChallengesTemplate" and "HTBSherlocksTemplate" go in the matching config values.
+
+- to get "in progress" todo colour - add to trilium demo/scripting/taskmanager/implementation/CSS:
+
+```
+ span.fancytree-node.inprogress .fancytree-title {
+    color: orange !important;
+ }
+```
\ No newline at end of file
diff --git a/htb2trilium/README.md b/htb2trilium/README.md
new file mode 100644
index 0000000..1b45ae2
--- /dev/null
+++ b/htb2trilium/README.md
@@ -0,0 +1,16 @@
+htb2trilium
+===============
+
+Pull hackthebox info and stats into personal trilium note taking software
+
+single machine view:
+
+![machine view](images/machine_view.png)
+
+overview of machines:
+
+![machines overview](images/machines_overview.png)
+
+navigation:
+
+![navigation](images/navigation.png)
\ No newline at end of file
diff --git a/htb2trilium/config.json b/htb2trilium/config.json
new file mode 100644
index 0000000..39597ce
--- /dev/null
+++ b/htb2trilium/config.json
@@ -0,0 +1,11 @@
+{
+	"htb_code": "in https://app.hackthebox.com/profile/settings click 'profile settings' and under 'app tokens' you can 'create app token' - that goes here",
+	"trilium_server_url": "https://place.to.your.trilium.server",
+	"trilium_token": "",
+	"trilium_machines_htb_folder": "",
+	"trilium_machines_template_id": "",
+	"trilium_challenges_folder": "",
+	"trilium_challenges_template_id": "",
+	"trilium_sherlocks_folder": "",
+	"trilium_sherlocks_template_id": ""
+}
\ No newline at end of file
diff --git a/htb2trilium/htb2trilium_challenges.py b/htb2trilium/htb2trilium_challenges.py
new file mode 100644
index 0000000..d0d95ca
--- /dev/null
+++ b/htb2trilium/htb2trilium_challenges.py
@@ -0,0 +1,146 @@
+import os
+import json
+import requests
+from collections import defaultdict
+from bs4 import BeautifulSoup
+from datetime import datetime, timezone
+from htb_client import HTBClient
+from trilium_py.client import ETAPI
+
+# Get the absolute path of the script's directory
+script_dir = os.path.dirname(os.path.abspath(__file__))
+
+# Construct the full path to the config.json file
+config_path = os.path.join(script_dir, 'config.json')
+
+# Load configuration from the JSON file
+with open(config_path, 'r') as f:
+    config = json.load(f)
+
+# Accessing config values
+htb_code = config['htb_code']
+trilium_server_url = config['trilium_server_url']
+trilium_token = config['trilium_token']
+trilium_challenges_folder = config['trilium_challenges_folder']
+trilium_challenges_template_id = config['trilium_challenges_template_id']
+
+def get_timestamp(machine):
+    # Parse the release date string into a datetime object
+    release_str = machine['release_date']
+    dt = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ")
+    
+    # Set the timezone to UTC
+    dt = dt.replace(tzinfo=timezone.utc)
+    
+    # Get the timestamp
+    timestamp = dt.timestamp()
+    return timestamp
+
+print("[+] connecting to HTB")
+client = HTBClient(password=htb_code)
+print("[+] connecting to trilium")
+ea = ETAPI(trilium_server_url, trilium_token)
+print("[i] version: ", ea.app_info()['appVersion'])
+
+print("[i] HTB User:", client.user['id'], "-", client.user['name'])
+
+
+print("[+] gathering challenges info")
+categories = defaultdict(list)
+challenges = client.get_all_challenges()
+challenges.sort(key=get_timestamp)
+total_completed = 0  # Variable to track total completed challenges
+
+print(f"[i] Retrieved {len(challenges)} challenges")
+
+# Group challenges by their categories
+for challenge in challenges:
+    categories[challenge['category_name']].append(challenge)
+    if challenge['is_owned']:
+        total_completed += 1  # Increment total completed if challenge is owned
+
+# Print out the grouped challenges with the number of completed and total challenges in each category
+for category, grouped_challenges in categories.items():
+    total = len(grouped_challenges)
+    completed = sum(1 for challenge in grouped_challenges if challenge['is_owned'])  # Count completed challenges
+    
+    res = ea.search_note(
+        search=f"note.title %= '{category}*'",
+        ancestorNoteId=trilium_challenges_folder,
+        ancestorDepth='eq1',
+        limit=1,
+        fastSearch=True,
+    )
+    
+    catId = ""
+    if res['results'] and res['results'][0]['title'].split(' - ')[0].strip().lower() == category.lower():
+        # page exists - lets check if the details have changed
+        ea.patch_note(noteId=res['results'][0]['noteId'], title=category+" - "+str(completed)+" / "+str(total))
+        catId = res['results'][0]['noteId']
+        print(f"[i] updated category: {category} - ({completed}/{total})")
+    else:
+        new_note = ea.create_note(
+            parentNoteId=trilium_challenges_folder,
+            type="text",
+            title=category+" - "+str(completed)+" / "+str(total),
+            content=" ",
+        )
+        catId = new_note['note']['noteId']
+        print(f"[+] created category: {catId} {category} - ({completed}/{total})")
+    
+    for challenge in grouped_challenges:
+        #print(f" - ID: {challenge['id']}, Name: {challenge['name']}, Difficulty: {challenge['difficulty']}")
+        res2 = ea.search_note(
+            search=f"{challenge['name']}",
+            ancestorNoteId=catId,
+            ancestorDepth='eq1',
+            limit=1,
+            fastSearch=True,
+        )
+        # already exists update the values
+        if res2['results'] and res2['results'][0]['title'].lower() == challenge['name'].lower():
+            print(f"[i] found ID: {challenge['id']}, Name: {challenge['name']}, Difficulty: {challenge['difficulty']}")
+            #print(f"Search response for challenge '{challenge['name']}': {res2}")
+            for attribute in res2['results'][0]['attributes']:
+                if attribute['name'] == "Difficulty":
+                    ea.patch_attribute(attributeId=attribute['attributeId'], value=challenge['difficulty'])
+                if attribute['name'] == "Released":
+                    release_str = challenge['release_date']
+                    release_date = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ")
+                    formatted_release_date = release_date.strftime("%d %B %Y")
+                    ea.patch_attribute(attributeId=attribute['attributeId'], value=formatted_release_date)
+                if attribute['name'] == "Solved":
+                    if challenge['is_owned']:
+                        ea.patch_attribute(attributeId=attribute['attributeId'], value="done")
+                    else:
+                        ea.patch_attribute(attributeId=attribute['attributeId'], value=" ")
+                if attribute['name'] == "cssClass":
+                    if challenge['is_owned']:
+                        ea.patch_attribute(attributeId=attribute['attributeId'], value="done")
+                    else:
+                        ea.patch_attribute(attributeId=attribute['attributeId'], value="todo")
+
+        else: # doesnt already exist, create page
+            release_str = challenge['release_date']
+            release_date = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ")
+            formatted_release_date = release_date.strftime("%d %B %Y")
+            new_note = ea.create_note(
+                parentNoteId=catId,
+                type="text",
+                title=challenge['name'],
+                content=" ",
+            )
+            ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="relation", name="template", value=trilium_challenges_template_id)
+            ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Difficulty", value=challenge['difficulty'])
+            ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Released", value=formatted_release_date)
+            if challenge['is_owned']:
+                ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="done")
+                ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Solved", value="done")
+            else:
+                ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="todo")
+                ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Solved", value=" ")
+
+            print(f"[+] created ID: {challenge['id']}, Name: {challenge['name']}, Difficulty: {challenge['difficulty']}")
+
+print("[+] updating folder name                                          ")
+ea.patch_note(noteId=trilium_challenges_folder,title="Challenges - "+str(total_completed)+" / "+str(len(challenges)))

diff --git a/README.md b/README.md
index f623b41..fefccb6 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,58 @@
 TriliumScripts
 ===============
 
-My collection of scripts for use with trilium
\ No newline at end of file
+Collection of scripts for use with trilium
+
+## syntax highlighting
+
+**highlight_moodlog.js** & **highlight_todo.js** - Both of these will apply appropriate syntaxy highlighting to the specific note.
+
+### installation
+
+1) add the code as the notes contents.
+
+2) set "note type" to "JS frontend"
+
+3) add an "owned attribute" of "#widget"
+
+### use
+
+on the note you want highlighting set the "owned attributes" to "#todotxt #hideHighlightWidget" or "#moodlog #hideHighlightWidget" depending on which syntax type you want to highlight
+
+## bidirectional sync
+
+**trilium_sync.php** - script to keep a folder of local text files and a trilium note in sync.
+
+### installation
+
+Edit the config values at the top of the script to match your trilium setup. Run it manually to test it is working as expected. If it is create a cron job.
+
+### example use
+
+```
+$> php trilium_sync.php 
+[i] checking connections
+[i] version: x.xx.x
+[i] gathering info
+
+- Folders -
+Folder/Path              Folder Name              Parent       noteId       Mod Trl    Mod Dsk    Status         
+Synced/                  Synced                   root         j0DRZhIzV97W 1742502936 1742507367 Exists
+Synced/BothPlaces/       BothPlaces               j0DRZhIzV97W AQb3Dg0yZHSu 1742507075 1742507200 Exists
+Synced/[del] tobe del/   [del] tobe del           j0DRZhIzV97W j0xUs1FdZL1Y 1742507321            Missing_Disk   
+Synced/AddToTrilium/     AddToTrilium                                                  1742507383 Missing_Trilium
+Synced/tobe deleted/     tobe deleted                                                  1742507200 Missing_Trilium
+- Files -
+Folder/Path              Filename                 Parent       noteId       Mod Trl    Mod Dsk    Hash  Status         
+Synced/BothPlaces/       UpdateDiskFile           AQb3Dg0yZHSu IwUZLwWdbiWG 1742507314 1742507199 Diff  Update_Disk    
+Synced/BothPlaces/       UpdateTriliumFile        AQb3Dg0yZHSu 7pMPgsb9AVRf 1742507130 1742507345 Diff  Update_Trilium 
+Synced/[del] tobe del/   gone_also.txt            j0xUs1FdZL1Y 4wOoHDy07csr 1742507178            N/A   Missing_Disk   
+Synced/AddToTrilium/     fromDiskToTrilium.txt                                         1742507394 N/A   Missing_Trilium
+Synced/tobe deleted/     gone_also.txt                                                 1742507200 N/A   Missing_Trilium
+
+[-] deleting disk folder: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/tobe deleted/
+[+] creating trilium folder: Synced/AddToTrilium/
+[+] creating trilium note: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/AddToTrilium/fromDiskToTrilium.txt
+[+] updating file on disk: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/BothPlaces/UpdateDiskFile
+[+] updating file on trilium: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/BothPlaces/UpdateTriliumFile
+```
\ No newline at end of file
diff --git a/highlight_moodlog.js b/highlight_moodlog.js
new file mode 100644
index 0000000..be4c283
--- /dev/null
+++ b/highlight_moodlog.js
@@ -0,0 +1,165 @@
+const TPL = `<div style="padding: 10px; border-top: 1px solid var(--main-border-color); contain: none;">
+    <strong>Moodlog Highlighting Applied</strong>
+</div>`;
+
+class MoodlogHighlighterWidget extends api.NoteContextAwareWidget {
+    static get parentWidget() { 
+        //console.log("MDLG: Getting parent widget: center-pane");
+        return 'center-pane'; 
+    }
+
+    get position() { 
+        //console.log("MDLG: Setting position: 100");
+        return 100; 
+    }
+
+    isEnabled() {
+        const enabled = super.isEnabled() 
+            && this.note.type === 'text' 
+            && this.note.hasLabel('moodlog');
+        //console.log(`MDLG: isEnabled: ${enabled}`);
+        return enabled;
+    }
+
+    doRender() {
+        //console.log("MDLG: Rendering widget...");
+        this.$widget = $(TPL);
+        //console.log("MDLG: Widget rendered:", this.$widget);
+        return this.$widget;
+    }
+
+    async refreshWithNote(note) {
+        //console.log("MDLG: Refreshing with note:", note);
+        const { content } = await note.getNoteComplement();
+        //console.log("MDLG: Note content fetched:", content);
+
+        // Check if content is fetched properly
+        if (!content) {
+            //console.log("MDLG: No content fetched. Exiting refresh.");
+            return;
+        }
+
+        const highlightedContent = this.applyHighlighting(content);
+        //console.log("MDLG: Highlighted content:", highlightedContent);
+
+        if (highlightedContent !== content) {
+            //console.log("MDLG: Updating note content with highlighted version");
+
+            // Use the setNoteContent function to update the note content
+            try {
+                await setNoteContent(note.noteId, highlightedContent);
+                //console.log("MDLG: Content updated with highlighted version");
+            } catch (error) {
+                //console.error("MDLG: Error updating content:", error);
+            }
+        } else {
+            //console.log("MDLG: No changes to content, skipping update.");
+        }
+    }
+
+
+ applyHighlighting(content) {
+    //console.log("MDLG: Applying highlighting to content...");
+
+    // Replace <br> with \n, then remove all other HTML tags
+    const plainTextContent = content.replace(/<br\s*\/?>/gi, '\n').replace(/<[^>]*>/g, ' ');
+    //console.log("MDLG: Stripped content:", plainTextContent);
+
+    // Combined regex for date & time
+    const dateTimePattern = /([1-2]\d{3}[-/.](0?[1-9]|1[0-2])[-/.](0[1-9]|[12]\d|3[01]))\s+(\d{1,2}:\d{2})/g;
+    const projectPattern = /\[\S+?\]/g;
+    const contextPattern = /(\s|^)(@\S+)/g;
+    const hashtagPattern = /(\s|^)(#\S+)/g;
+    const positivePattern = /(\s|^)(\+\S+)/g;
+    const negativePattern = /(\s|^)(-\S+)/g;
+
+    // Escape HTML characters to prevent issues with replacements
+    function escapeHtml(str) {
+        return str.replace(/[&<>"']/g, function (char) {
+            return {
+                '&': '&amp;',
+                '<': '&lt;',
+                '>': '&gt;',
+                '"': '&quot;',
+                "'": '&#039;'
+            }[char];
+        });
+    }
+
+      // Helper function to map the number to a colour based on the gradient
+    function getProjectColour(number) {
+        const minColor = { r: 107, g: 109, b: 229 };  // #6b6de5
+        const maxColor = { r: 70, g: 226, b: 96 };   // #46e260
+
+        const ratio = number / 9; // since we are working with 0-9
+
+        // Interpolate the RGB values between minColor and maxColor
+        const r = Math.round(minColor.r + ratio * (maxColor.r - minColor.r));
+        const g = Math.round(minColor.g + ratio * (maxColor.g - minColor.g));
+        const b = Math.round(minColor.b + ratio * (maxColor.b - minColor.b));
+
+        return `rgb(${r}, ${g}, ${b})`;
+    }
+     
+    // Process each line of the plain text content
+    let highlightedLines = plainTextContent.split("\n").map(line => {
+        //console.log("MDLG: Processing line:", line);
+
+        // Apply combined date & time highlight
+        line = line.replace(dateTimePattern, `<span style="color: rgb(188, 55, 237);">$1</span> <span style="color: rgb(188, 55, 237);">$4</span>`);
+
+        // Apply other highlights
+        line = line.replace(projectPattern, (match) => {
+            // Extract the number inside square brackets
+            const number = parseInt(match.match(/\d+/)[0], 10);
+            const colour = getProjectColour(number);
+            return `<span style="color: ${colour}; font-weight: bold;">${escapeHtml(match)}</span>`;
+        });
+        
+        // Apply other highlights
+        //line = line.replace(projectPattern, `<span style="color: rgb(224, 218, 36); font-weight: bold;">$&</span>`);
+        line = line.replace(contextPattern, (_, space, tag) => `${space}<span style="color: rgb(255 165 10);">${escapeHtml(tag)}</span>`);
+        line = line.replace(hashtagPattern, (_, space, tag) => `${space}<span style="color: rgb(41, 229, 242);">${escapeHtml(tag)}</span>`);
+        line = line.replace(positivePattern, (_, space, tag) => `${space}<span style="color: rgb(44, 227, 34);">${escapeHtml(tag)}</span>`);
+        line = line.replace(negativePattern, (_, space, tag) => `${space}<span style="color: rgb(230, 59, 44);">${escapeHtml(tag)}</span>`);
+
+        return line.trim();
+    });
+
+    // Join lines with proper <br> handling
+    let finalContent = highlightedLines.join("<br>");
+
+    //console.log("MDLG: Highlighted content:", finalContent);
+    return finalContent;
+}
+
+
+
+
+    async entitiesReloadedEvent({ loadResults }) {
+        //console.log("MDLG: Entities reloaded event triggered");
+        if (loadResults.isNoteContentReloaded(this.noteId)) {
+            //console.log("MDLG: Note content reloaded, refreshing widget...");
+            //this.refresh();
+        } else {
+            //console.log("MDLG: No content reload detected.");
+        }
+    }
+    
+
+}
+
+console.log("MDLG: Starting Moodlog Highlighter Widget");
+module.exports = MoodlogHighlighterWidget;
+
+function setNoteContent(noteId, content) {
+    /*const noteElement = document.querySelector(`note-detail-readonly-text-content`);
+
+    if (noteElement) {
+        noteElement.innerHTML = content; // Update the displayed content
+        console.log(`MDLG: Updated note ${noteId} in the DOM.`);
+    } else {
+        console.warn(`MDLG: Note element with data-note-id='${noteId}' not found.`);
+    }*/
+    return api.runOnBackend((id, data) => api.getNote(id).setContent(data), [noteId, content]);
+}
diff --git a/highlight_todotxt.js b/highlight_todotxt.js
new file mode 100644
index 0000000..1f1b007
--- /dev/null
+++ b/highlight_todotxt.js
@@ -0,0 +1,157 @@
+const TPL = `<div style="padding: 10px; border-top: 1px solid var(--main-border-color); contain: none;">
+    <strong>Todo Highlighting Applied</strong>
+</div>`;
+
+class MoodlogHighlighterWidget extends api.NoteContextAwareWidget {
+    static get parentWidget() { 
+        //console.log("MDLG: Getting parent widget: center-pane");
+        return 'center-pane'; 
+    }
+
+    get position() { 
+        //console.log("MDLG: Setting position: 100");
+        return 100; 
+    }
+
+    isEnabled() {
+        const enabled = super.isEnabled() 
+            && this.note.type === 'text' 
+            && this.note.hasLabel('todotxt');
+        //console.log(`MDLG: isEnabled: ${enabled}`);
+        return enabled;
+    }
+
+    doRender() {
+        //console.log("MDLG: Rendering widget...");
+        this.$widget = $(TPL);
+        //console.log("MDLG: Widget rendered:", this.$widget);
+        return this.$widget;
+    }
+
+    async refreshWithNote(note) {
+        //console.log("MDLG: Refreshing with note:", note);
+        const { content } = await note.getNoteComplement();
+        //console.log("MDLG: Note content fetched:", content);
+
+        // Check if content is fetched properly
+        if (!content) {
+            //console.log("MDLG: No content fetched. Exiting refresh.");
+            return;
+        }
+
+        const highlightedContent = this.applyHighlighting(content);
+        //console.log("MDLG: Highlighted content:", highlightedContent);
+
+        if (highlightedContent !== content) {
+            //console.log("MDLG: Updating note content with highlighted version");
+
+            // Use the setNoteContent function to update the note content
+            try {
+                await setNoteContent(note.noteId, highlightedContent);
+                //console.log("MDLG: Content updated with highlighted version");
+            } catch (error) {
+                //console.error("MDLG: Error updating content:", error);
+            }
+        } else {
+            //console.log("MDLG: No changes to content, skipping update.");
+        }
+    }
+
+
+applyHighlighting(content) {
+    // Replace <br> with \n, then remove all other HTML tags
+    const plainTextContent = content.replace(/<br\s*\/?\>/gi, '\n').replace(/<[^>]*>/g, ' ');
+
+    // Regular expressions for Todo.txt syntax
+    const datePattern = /\b(0[1-9]|[12]\d|3[01])\/(0[1-9]|1[0-2])\/([1-2]\d{3})\b/g;
+    const completedTaskPattern = /^\s*x\s+.*/g;
+    const priorityPattern = /^(\s*\([A-E]\))\s+/;
+    const projectPattern = /(?:\s|^)(\+\S+)/g;
+    const contextPattern = /(?:\s|^)(@\S+)/g;
+    const waitPattern = /\bWAIT\b/gi;
+    const customAttributePattern = /(\s+[^\s:]+:[^\s:]+)+\s*$/g;
+    
+    function escapeHtml(str) {
+        return str.replace(/[&<>"']/g, char => ({
+            '&': '&amp;', '<': '&lt;', '>': '&gt;',
+            '"': '&quot;', "'": '&#039;'
+        })[char]);
+    }
+
+    let highlightedLines = plainTextContent.split("\n").map(line => {
+        // If line starts with 'x', make the whole line grey
+        if (/^\s*x\s+/.test(line)) {
+            return `<span style="color: grey;">${line}</span>`;
+        }
+        
+        // Highlight priority with different colours based on level
+        line = line.replace(priorityPattern, (match, priority) => {
+            const level = priority.match(/[A-E]/)?.[0];
+            const colors = {
+                'A': 'rgb(255, 0, 0)',       // Red
+                'B': 'rgb(255, 102, 0)',     // Orange
+                'C': 'rgb(255, 204, 0)',     // Yellow
+                'D': 'rgb(0, 153, 255)',     // Blue
+                'E': 'rgb(102, 204, 102)'    // Green
+            };
+            const color = colors[level] || 'black';
+            return `<span style="color: ${color}; font-weight: bold;">${escapeHtml(priority)}</span> `;
+        });
+        
+        // Highlight date
+        line = line.replace(datePattern, (match) => {
+            return `<span style="color: rgb(188, 55, 237);">${escapeHtml(match)}</span>`;
+        });
+
+        // Highlight project tags
+        line = line.replace(projectPattern, (_, tag) => `<span style="color: rgb(224, 218, 36);"> ${escapeHtml(tag)}</span>`);
+        
+        // Highlight context tags
+        line = line.replace(contextPattern, (_, tag) => `<span style="color: rgb(255, 165, 10);"> ${escapeHtml(tag)}</span>`);
+        
+        // Highlight WAIT command
+        line = line.replace(waitPattern, `<span style="color: rgb(255, 69, 0);">WAIT</span>`);
+        
+        // Highlight custom attributes
+        line = line.replace(customAttributePattern, `<span style="color: rgb(100, 149, 237);">$1</span>`);
+        
+        // Remove leading space (if any) from the beginning of the line, without affecting spaces before + or @
+        line = line.replace(/^\s+/, '');
+        
+        return line.trim();
+    });
+
+    return highlightedLines.join("<br>");
+}
+
+
+
+
+
+    async entitiesReloadedEvent({ loadResults }) {
+        //console.log("MDLG: Entities reloaded event triggered");
+        if (loadResults.isNoteContentReloaded(this.noteId)) {
+            //console.log("MDLG: Note content reloaded, refreshing widget...");
+            //this.refresh();
+        } else {
+            //console.log("MDLG: No content reload detected.");
+        }
+    }
+    
+
+}
+
+console.log("TodoHighlight: Starting todotxt Highlighter Widget");
+module.exports = MoodlogHighlighterWidget;
+
+function setNoteContent(noteId, content) {
+    /*const noteElement = document.querySelector(`note-detail-readonly-text-content`);
+
+    if (noteElement) {
+        noteElement.innerHTML = content; // Update the displayed content
+        console.log(`MDLG: Updated note ${noteId} in the DOM.`);
+    } else {
+        console.warn(`MDLG: Note element with data-note-id='${noteId}' not found.`);
+    }*/
+    return api.runOnBackend((id, data) => api.getNote(id).setContent(data), [noteId, content]);
+}
diff --git a/htb2trilium/INSTALL.md b/htb2trilium/INSTALL.md
new file mode 100644
index 0000000..da8afad
--- /dev/null
+++ b/htb2trilium/INSTALL.md
@@ -0,0 +1,41 @@
+## scripts require:
+
+```
+ > pip3 install trilium-py
+```
+
+## config.json
+- get trilium token in trilium options->ETAPI and "create new token"
+- create a new note with a title "Machines"
+- click on this note -> "note info". Note ID is there. This ID goes in "trilium_machines_htb_folder" in the config file
+- set the folder's owned attributes to:
+
+```
+#label:user=promoted,single,text #label:root=promoted,single,text #label:respect=promoted,single,text #user=0 #root=0 #respect=0 
+```
+
+ - create a note named "HTBMachineTemplate" 
+ - set it's "owned attributes" to:
+
+```
+#template #label:User=promoted,single,text #label:Root=promoted,single,text #label:Tags=promoted,single,text
+```
+
+- get this page's ID and put in "trilium_machines_template_id"
+- create "challenges" page, this page's ID goes in: trilium_challenges_folder  
+- create "Sherlocks" page, this page's ID goes in: trilium_sherlocks_folder  
+- create 2 pages "HTBChallengesTemplate" and "HTBSherlocksTemplate", both of these should have the owned attributes:
+
+```
+#template #label:Difficulty=promoted,single,text #label:Solved=promoted,single,text #label:Released=promoted,single,text 
+```
+
+- The ID's of "HTBChallengesTemplate" and "HTBSherlocksTemplate" go in the matching config values.
+
+- to get "in progress" todo colour - add to trilium demo/scripting/taskmanager/implementation/CSS:
+
+```
+ span.fancytree-node.inprogress .fancytree-title {
+    color: orange !important;
+ }
+```
\ No newline at end of file
diff --git a/htb2trilium/README.md b/htb2trilium/README.md
new file mode 100644
index 0000000..1b45ae2
--- /dev/null
+++ b/htb2trilium/README.md
@@ -0,0 +1,16 @@
+htb2trilium
+===============
+
+Pull hackthebox info and stats into personal trilium note taking software
+
+single machine view:
+
+![machine view](images/machine_view.png)
+
+overview of machines:
+
+![machines overview](images/machines_overview.png)
+
+navigation:
+
+![navigation](images/navigation.png)
\ No newline at end of file
diff --git a/htb2trilium/config.json b/htb2trilium/config.json
new file mode 100644
index 0000000..39597ce
--- /dev/null
+++ b/htb2trilium/config.json
@@ -0,0 +1,11 @@
+{
+	"htb_code": "in https://app.hackthebox.com/profile/settings click 'profile settings' and under 'app tokens' you can 'create app token' - that goes here",
+	"trilium_server_url": "https://place.to.your.trilium.server",
+	"trilium_token": "",
+	"trilium_machines_htb_folder": "",
+	"trilium_machines_template_id": "",
+	"trilium_challenges_folder": "",
+	"trilium_challenges_template_id": "",
+	"trilium_sherlocks_folder": "",
+	"trilium_sherlocks_template_id": ""
+}
\ No newline at end of file
diff --git a/htb2trilium/htb2trilium_challenges.py b/htb2trilium/htb2trilium_challenges.py
new file mode 100644
index 0000000..d0d95ca
--- /dev/null
+++ b/htb2trilium/htb2trilium_challenges.py
@@ -0,0 +1,146 @@
+import os
+import json
+import requests
+from collections import defaultdict
+from bs4 import BeautifulSoup
+from datetime import datetime, timezone
+from htb_client import HTBClient
+from trilium_py.client import ETAPI
+
+# Get the absolute path of the script's directory
+script_dir = os.path.dirname(os.path.abspath(__file__))
+
+# Construct the full path to the config.json file
+config_path = os.path.join(script_dir, 'config.json')
+
+# Load configuration from the JSON file
+with open(config_path, 'r') as f:
+    config = json.load(f)
+
+# Accessing config values
+htb_code = config['htb_code']
+trilium_server_url = config['trilium_server_url']
+trilium_token = config['trilium_token']
+trilium_challenges_folder = config['trilium_challenges_folder']
+trilium_challenges_template_id = config['trilium_challenges_template_id']
+
+def get_timestamp(machine):
+    # Parse the release date string into a datetime object
+    release_str = machine['release_date']
+    dt = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ")
+    
+    # Set the timezone to UTC
+    dt = dt.replace(tzinfo=timezone.utc)
+    
+    # Get the timestamp
+    timestamp = dt.timestamp()
+    return timestamp
+
+print("[+] connecting to HTB")
+client = HTBClient(password=htb_code)
+print("[+] connecting to trilium")
+ea = ETAPI(trilium_server_url, trilium_token)
+print("[i] version: ", ea.app_info()['appVersion'])
+
+print("[i] HTB User:", client.user['id'], "-", client.user['name'])
+
+
+print("[+] gathering challenges info")
+categories = defaultdict(list)
+challenges = client.get_all_challenges()
+challenges.sort(key=get_timestamp)
+total_completed = 0  # Variable to track total completed challenges
+
+print(f"[i] Retrieved {len(challenges)} challenges")
+
+# Group challenges by their categories
+for challenge in challenges:
+    categories[challenge['category_name']].append(challenge)
+    if challenge['is_owned']:
+        total_completed += 1  # Increment total completed if challenge is owned
+
+# Print out the grouped challenges with the number of completed and total challenges in each category
+for category, grouped_challenges in categories.items():
+    total = len(grouped_challenges)
+    completed = sum(1 for challenge in grouped_challenges if challenge['is_owned'])  # Count completed challenges
+    
+    res = ea.search_note(
+        search=f"note.title %= '{category}*'",
+        ancestorNoteId=trilium_challenges_folder,
+        ancestorDepth='eq1',
+        limit=1,
+        fastSearch=True,
+    )
+    
+    catId = ""
+    if res['results'] and res['results'][0]['title'].split(' - ')[0].strip().lower() == category.lower():
+        # page exists - lets check if the details have changed
+        ea.patch_note(noteId=res['results'][0]['noteId'], title=category+" - "+str(completed)+" / "+str(total))
+        catId = res['results'][0]['noteId']
+        print(f"[i] updated category: {category} - ({completed}/{total})")
+    else:
+        new_note = ea.create_note(
+            parentNoteId=trilium_challenges_folder,
+            type="text",
+            title=category+" - "+str(completed)+" / "+str(total),
+            content=" ",
+        )
+        catId = new_note['note']['noteId']
+        print(f"[+] created category: {catId} {category} - ({completed}/{total})")
+    
+    for challenge in grouped_challenges:
+        #print(f" - ID: {challenge['id']}, Name: {challenge['name']}, Difficulty: {challenge['difficulty']}")
+        res2 = ea.search_note(
+            search=f"{challenge['name']}",
+            ancestorNoteId=catId,
+            ancestorDepth='eq1',
+            limit=1,
+            fastSearch=True,
+        )
+        # already exists update the values
+        if res2['results'] and res2['results'][0]['title'].lower() == challenge['name'].lower():
+            print(f"[i] found ID: {challenge['id']}, Name: {challenge['name']}, Difficulty: {challenge['difficulty']}")
+            #print(f"Search response for challenge '{challenge['name']}': {res2}")
+            for attribute in res2['results'][0]['attributes']:
+                if attribute['name'] == "Difficulty":
+                    ea.patch_attribute(attributeId=attribute['attributeId'], value=challenge['difficulty'])
+                if attribute['name'] == "Released":
+                    release_str = challenge['release_date']
+                    release_date = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ")
+                    formatted_release_date = release_date.strftime("%d %B %Y")
+                    ea.patch_attribute(attributeId=attribute['attributeId'], value=formatted_release_date)
+                if attribute['name'] == "Solved":
+                    if challenge['is_owned']:
+                        ea.patch_attribute(attributeId=attribute['attributeId'], value="done")
+                    else:
+                        ea.patch_attribute(attributeId=attribute['attributeId'], value=" ")
+                if attribute['name'] == "cssClass":
+                    if challenge['is_owned']:
+                        ea.patch_attribute(attributeId=attribute['attributeId'], value="done")
+                    else:
+                        ea.patch_attribute(attributeId=attribute['attributeId'], value="todo")
+
+        else: # doesnt already exist, create page
+            release_str = challenge['release_date']
+            release_date = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ")
+            formatted_release_date = release_date.strftime("%d %B %Y")
+            new_note = ea.create_note(
+                parentNoteId=catId,
+                type="text",
+                title=challenge['name'],
+                content=" ",
+            )
+            ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="relation", name="template", value=trilium_challenges_template_id)
+            ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Difficulty", value=challenge['difficulty'])
+            ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Released", value=formatted_release_date)
+            if challenge['is_owned']:
+                ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="done")
+                ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Solved", value="done")
+            else:
+                ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="todo")
+                ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Solved", value=" ")
+
+            print(f"[+] created ID: {challenge['id']}, Name: {challenge['name']}, Difficulty: {challenge['difficulty']}")
+
+print("[+] updating folder name                                          ")
+ea.patch_note(noteId=trilium_challenges_folder,title="Challenges - "+str(total_completed)+" / "+str(len(challenges)))
diff --git a/htb2trilium/htb2trilium_machines.py b/htb2trilium/htb2trilium_machines.py
new file mode 100644
index 0000000..de8be53
--- /dev/null
+++ b/htb2trilium/htb2trilium_machines.py
@@ -0,0 +1,203 @@
+import os
+import json
+import requests
+from bs4 import BeautifulSoup
+from datetime import datetime, timezone
+from htb_client import HTBClient
+from trilium_py.client import ETAPI
+
+# Get the absolute path of the script's directory
+script_dir = os.path.dirname(os.path.abspath(__file__))
+
+# Construct the full path to the config.json file
+config_path = os.path.join(script_dir, 'config.json')
+
+# Load configuration from the JSON file
+with open(config_path, 'r') as f:
+    config = json.load(f)
+
+# Accessing config values
+htb_code = config['htb_code']
+trilium_server_url = config['trilium_server_url']
+trilium_token = config['trilium_token']
+trilium_machines_htb_folder = config['trilium_machines_htb_folder']
+trilium_machines_template_id = config['trilium_machines_template_id']
+
+def generate_newpage(machine):
+	no = "<span style=\"color:hsl(0,75%,60%);\">No</span>"
+	yes = "<span style=\"color:hsl(120,75%,60%);\">Yes</span>"
+	user_colour = no
+	if machine['authUserInUserOwns']:
+		user_colour = yes
+	root_colour = no
+	if machine['authUserInRootOwns']:
+		root_colour = yes
+
+	status = "Retired"
+	if machine['active']:
+		status = "Active"
+
+	release_str = machine['release']
+	release_date = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ")
+	formatted_release_date = release_date.strftime("%d %B %Y")
+
+	html = """
+	<figure class="image image-style-align-left image_resized" style="width:12.28%;">
+	  <img src="https://www.hackthebox.com{avatar}">
+	</figure>
+	<p>
+	  <strong>OS:</strong> {os}<br>
+	  <strong>Difficulty:</strong> {difficultyText} <br>
+	  <strong>Rating:</strong> {rating} / 5<br>
+	  <strong>Points:</strong> {points}<br>
+	  <strong>User / Root: </strong> {user_colour} / {root_colour}<br>
+	  <strong>Released:</strong> {release_date}<br>
+	  <strong>State:</strong> {status}
+	</p>
+	<hr>
+	<h2>Notes</h2>
+	<p>&nbsp;</p>
+	""".format(
+		os=machine['os'],
+		difficultyText=machine['difficultyText'],
+		rating=machine['star'],
+		points=machine['points'],
+		release_date=formatted_release_date,
+		user_colour=user_colour,
+		root_colour=root_colour,
+		status = status,
+		avatar = machine['avatar'],
+	)
+
+	return html
+
+def get_timestamp(machine):
+    # Parse the release date string into a datetime object
+    release_str = machine['release']
+    dt = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ")
+    
+    # Set the timezone to UTC
+    dt = dt.replace(tzinfo=timezone.utc)
+    
+    # Get the timestamp
+    timestamp = dt.timestamp()
+    return timestamp
+
+print("[+] connecting to HTB")
+client = HTBClient(password=htb_code)
+print("[+] connecting to trilium")
+ea = ETAPI(trilium_server_url, trilium_token)
+print("[i] version: ", ea.app_info()['appVersion'])
+
+print("[i] HTB User:", client.user['id'], "-", client.user['name'])
+print("[i] user owns:", client.user['user_owns'], "| Root owns:", client.user['root_owns'], "| Respect:", client.user['respects'])
+
+master_folder = ea.get_note(trilium_machines_htb_folder)
+for attribute in master_folder['attributes']:
+	if attribute['name'] == "user":
+		if attribute['value'] != str(client.user['user_owns']):
+			print("[+] updating user owns (folder attribute)")
+			ea.patch_attribute(attributeId=attribute['attributeId'], value=str(client.user['user_owns']))
+	if attribute['name'] == "root":
+		if attribute['value'] != str(client.user['root_owns']):
+			print("[+] updating root owns (folder attribute)")
+			ea.patch_attribute(attributeId=attribute['attributeId'], value=str(client.user['root_owns']))
+	if attribute['name'] == "respect":
+		if attribute['value'] != str(client.user['respects']):
+			print("[+] updating respect (folder attribute)")
+			ea.patch_attribute(attributeId=attribute['attributeId'], value=str(client.user['respects']))
+
+print("[+] gathering machines info")
+machines = client.get_all_machines()
+machines.sort(key=get_timestamp)
+
+print(f"[i] Retrieved {len(machines)} machines")
+#for machine in machines:
+#    print(f" - ID: {machine['id']}, Name: {machine['name']}, OS: {machine['os']}, Difficulty: {machine['difficultyText']}")
+
+machine_count = 0
+completed_count = 0
+for machine in machines:
+	machine_count += 1
+	print('processing: ',machine_count, "/", len(machines), "("+machine['name']+")               " , end='\r')
+
+	res = ea.search_note(
+		search="\""+machine['name']+"\"",
+		ancestorNoteId=trilium_machines_htb_folder,
+		ancestorDepth='eq1',
+		limit=1,
+		fastSearch=True,
+	)
+
+	if res['results'] and machine['name'] == res['results'][0]['title']:
+		# page exists - lets check if the details have changed
+
+		current_html = ea.get_note_content(noteId=res['results'][0]['noteId'])
+		current_soup = BeautifulSoup(current_html, 'html.parser')
+		current_paragraph = current_soup.find_all('p')[0].text
+
+		new_html = generate_newpage(machine)
+		new_soup = BeautifulSoup(new_html, 'html.parser')
+		new_paragraph = new_soup.find_all('p')[0].text
+
+		# current page contains first paragraph of "blank" (useful for when it doesnt create or find the note properly.. shouldnt get here)
+		if current_paragraph == "blank":
+			ea.update_note_content(noteId=res['results'][0]['noteId'], content=new_html)
+			ea.create_attribute(attributeId=None, isInheritable=False, noteId=res['results'][0]['noteId'], type="relation", name="template", value=trilium_machines_htb_folder)
+			if machine['authUserInUserOwns'] and machine['authUserInRootOwns']:
+				ea.create_attribute(attributeId=None, isInheritable=False, noteId=res['results'][0]['noteId'], type="label", name="cssClass", value="done")
+			else:
+				if machine['authUserInUserOwns'] or machine['authUserInRootOwns']:
+					ea.create_attribute(attributeId=None, isInheritable=False, noteId=res['results'][0]['noteId'], type="label", name="cssClass", value="inprogress")
+				else:
+					ea.create_attribute(attributeId=None, isInheritable=False, noteId=res['results'][0]['noteId'], type="label", name="cssClass", value="todo")
+			# re-get the current content
+			current_html = ea.get_note_content(noteId=res['results'][0]['noteId'])
+			current_soup = BeautifulSoup(current_html, 'html.parser')
+
+		if current_paragraph != new_paragraph:
+
+			# details have updated!
+			print("[+] updating page:",machine['name'], "-> "+res['results'][0]['title']+"                 ")
+			replacement = current_soup.find('p')
+			replacement.replace_with( new_soup.find_all('p')[0] )
+			ea.update_note_content(noteId=res['results'][0]['noteId'], content=current_soup)
+
+			# now to update the label
+			for attribute in res['results'][0]['attributes']:
+				if attribute['name'] == "cssClass":
+					if machine['authUserInUserOwns'] and machine['authUserInRootOwns']:
+						ea.patch_attribute(attributeId=attribute['attributeId'], value="done")
+					else:
+						if machine['authUserInUserOwns'] or machine['authUserInRootOwns']:
+							ea.patch_attribute(attributeId=attribute['attributeId'], value="inprogress")
+						else:
+							ea.patch_attribute(attributeId=attribute['attributeId'], value="todo")
+							
+	else:
+		# title does not exist - create the note
+		html = generate_newpage(machine)
+		new_note = ea.create_note(
+			parentNoteId=trilium_machines_htb_folder,
+			type="text",
+			title=machine['name'],
+			content=html,
+		)
+		print("[+] created note:", machine['name'], "                        ")
+
+		ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="relation", name="template", value=trilium_machines_htb_folder)
+		if machine['authUserInUserOwns'] and machine['authUserInRootOwns']:
+			ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="done")
+		else:
+			if machine['authUserInUserOwns'] or machine['authUserInRootOwns']:
+				ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="inprogress")
+			else:
+				ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="todo")
+
+	if machine['authUserInUserOwns'] and machine['authUserInRootOwns']:
+			completed_count += 1
+
+print("[+] updating folder name                                          ")
+ea.patch_note(noteId=trilium_machines_htb_folder,title="Machines - "+str(completed_count)+" / "+str(len(machines)))
+
+print("[=] processed", machine_count)
\ No newline at end of file

diff --git a/README.md b/README.md
index f623b41..fefccb6 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,58 @@
 TriliumScripts
 ===============
 
-My collection of scripts for use with trilium
\ No newline at end of file
+Collection of scripts for use with trilium
+
+## syntax highlighting
+
+**highlight_moodlog.js** & **highlight_todo.js** - Both of these will apply appropriate syntaxy highlighting to the specific note.
+
+### installation
+
+1) add the code as the notes contents.
+
+2) set "note type" to "JS frontend"
+
+3) add an "owned attribute" of "#widget"
+
+### use
+
+on the note you want highlighting set the "owned attributes" to "#todotxt #hideHighlightWidget" or "#moodlog #hideHighlightWidget" depending on which syntax type you want to highlight
+
+## bidirectional sync
+
+**trilium_sync.php** - script to keep a folder of local text files and a trilium note in sync.
+
+### installation
+
+Edit the config values at the top of the script to match your trilium setup. Run it manually to test it is working as expected. If it is create a cron job.
+
+### example use
+
+```
+$> php trilium_sync.php 
+[i] checking connections
+[i] version: x.xx.x
+[i] gathering info
+
+- Folders -
+Folder/Path              Folder Name              Parent       noteId       Mod Trl    Mod Dsk    Status         
+Synced/                  Synced                   root         j0DRZhIzV97W 1742502936 1742507367 Exists
+Synced/BothPlaces/       BothPlaces               j0DRZhIzV97W AQb3Dg0yZHSu 1742507075 1742507200 Exists
+Synced/[del] tobe del/   [del] tobe del           j0DRZhIzV97W j0xUs1FdZL1Y 1742507321            Missing_Disk   
+Synced/AddToTrilium/     AddToTrilium                                                  1742507383 Missing_Trilium
+Synced/tobe deleted/     tobe deleted                                                  1742507200 Missing_Trilium
+- Files -
+Folder/Path              Filename                 Parent       noteId       Mod Trl    Mod Dsk    Hash  Status         
+Synced/BothPlaces/       UpdateDiskFile           AQb3Dg0yZHSu IwUZLwWdbiWG 1742507314 1742507199 Diff  Update_Disk    
+Synced/BothPlaces/       UpdateTriliumFile        AQb3Dg0yZHSu 7pMPgsb9AVRf 1742507130 1742507345 Diff  Update_Trilium 
+Synced/[del] tobe del/   gone_also.txt            j0xUs1FdZL1Y 4wOoHDy07csr 1742507178            N/A   Missing_Disk   
+Synced/AddToTrilium/     fromDiskToTrilium.txt                                         1742507394 N/A   Missing_Trilium
+Synced/tobe deleted/     gone_also.txt                                                 1742507200 N/A   Missing_Trilium
+
+[-] deleting disk folder: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/tobe deleted/
+[+] creating trilium folder: Synced/AddToTrilium/
+[+] creating trilium note: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/AddToTrilium/fromDiskToTrilium.txt
+[+] updating file on disk: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/BothPlaces/UpdateDiskFile
+[+] updating file on trilium: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/BothPlaces/UpdateTriliumFile
+```
\ No newline at end of file
diff --git a/highlight_moodlog.js b/highlight_moodlog.js
new file mode 100644
index 0000000..be4c283
--- /dev/null
+++ b/highlight_moodlog.js
@@ -0,0 +1,165 @@
+const TPL = `<div style="padding: 10px; border-top: 1px solid var(--main-border-color); contain: none;">
+    <strong>Moodlog Highlighting Applied</strong>
+</div>`;
+
+class MoodlogHighlighterWidget extends api.NoteContextAwareWidget {
+    static get parentWidget() { 
+        //console.log("MDLG: Getting parent widget: center-pane");
+        return 'center-pane'; 
+    }
+
+    get position() { 
+        //console.log("MDLG: Setting position: 100");
+        return 100; 
+    }
+
+    isEnabled() {
+        const enabled = super.isEnabled() 
+            && this.note.type === 'text' 
+            && this.note.hasLabel('moodlog');
+        //console.log(`MDLG: isEnabled: ${enabled}`);
+        return enabled;
+    }
+
+    doRender() {
+        //console.log("MDLG: Rendering widget...");
+        this.$widget = $(TPL);
+        //console.log("MDLG: Widget rendered:", this.$widget);
+        return this.$widget;
+    }
+
+    async refreshWithNote(note) {
+        //console.log("MDLG: Refreshing with note:", note);
+        const { content } = await note.getNoteComplement();
+        //console.log("MDLG: Note content fetched:", content);
+
+        // Check if content is fetched properly
+        if (!content) {
+            //console.log("MDLG: No content fetched. Exiting refresh.");
+            return;
+        }
+
+        const highlightedContent = this.applyHighlighting(content);
+        //console.log("MDLG: Highlighted content:", highlightedContent);
+
+        if (highlightedContent !== content) {
+            //console.log("MDLG: Updating note content with highlighted version");
+
+            // Use the setNoteContent function to update the note content
+            try {
+                await setNoteContent(note.noteId, highlightedContent);
+                //console.log("MDLG: Content updated with highlighted version");
+            } catch (error) {
+                //console.error("MDLG: Error updating content:", error);
+            }
+        } else {
+            //console.log("MDLG: No changes to content, skipping update.");
+        }
+    }
+
+
+ applyHighlighting(content) {
+    //console.log("MDLG: Applying highlighting to content...");
+
+    // Replace <br> with \n, then remove all other HTML tags
+    const plainTextContent = content.replace(/<br\s*\/?>/gi, '\n').replace(/<[^>]*>/g, ' ');
+    //console.log("MDLG: Stripped content:", plainTextContent);
+
+    // Combined regex for date & time
+    const dateTimePattern = /([1-2]\d{3}[-/.](0?[1-9]|1[0-2])[-/.](0[1-9]|[12]\d|3[01]))\s+(\d{1,2}:\d{2})/g;
+    const projectPattern = /\[\S+?\]/g;
+    const contextPattern = /(\s|^)(@\S+)/g;
+    const hashtagPattern = /(\s|^)(#\S+)/g;
+    const positivePattern = /(\s|^)(\+\S+)/g;
+    const negativePattern = /(\s|^)(-\S+)/g;
+
+    // Escape HTML characters to prevent issues with replacements
+    function escapeHtml(str) {
+        return str.replace(/[&<>"']/g, function (char) {
+            return {
+                '&': '&amp;',
+                '<': '&lt;',
+                '>': '&gt;',
+                '"': '&quot;',
+                "'": '&#039;'
+            }[char];
+        });
+    }
+
+      // Helper function to map the number to a colour based on the gradient
+    function getProjectColour(number) {
+        const minColor = { r: 107, g: 109, b: 229 };  // #6b6de5
+        const maxColor = { r: 70, g: 226, b: 96 };   // #46e260
+
+        const ratio = number / 9; // since we are working with 0-9
+
+        // Interpolate the RGB values between minColor and maxColor
+        const r = Math.round(minColor.r + ratio * (maxColor.r - minColor.r));
+        const g = Math.round(minColor.g + ratio * (maxColor.g - minColor.g));
+        const b = Math.round(minColor.b + ratio * (maxColor.b - minColor.b));
+
+        return `rgb(${r}, ${g}, ${b})`;
+    }
+     
+    // Process each line of the plain text content
+    let highlightedLines = plainTextContent.split("\n").map(line => {
+        //console.log("MDLG: Processing line:", line);
+
+        // Apply combined date & time highlight
+        line = line.replace(dateTimePattern, `<span style="color: rgb(188, 55, 237);">$1</span> <span style="color: rgb(188, 55, 237);">$4</span>`);
+
+        // Apply other highlights
+        line = line.replace(projectPattern, (match) => {
+            // Extract the number inside square brackets
+            const number = parseInt(match.match(/\d+/)[0], 10);
+            const colour = getProjectColour(number);
+            return `<span style="color: ${colour}; font-weight: bold;">${escapeHtml(match)}</span>`;
+        });
+        
+        // Apply other highlights
+        //line = line.replace(projectPattern, `<span style="color: rgb(224, 218, 36); font-weight: bold;">$&</span>`);
+        line = line.replace(contextPattern, (_, space, tag) => `${space}<span style="color: rgb(255 165 10);">${escapeHtml(tag)}</span>`);
+        line = line.replace(hashtagPattern, (_, space, tag) => `${space}<span style="color: rgb(41, 229, 242);">${escapeHtml(tag)}</span>`);
+        line = line.replace(positivePattern, (_, space, tag) => `${space}<span style="color: rgb(44, 227, 34);">${escapeHtml(tag)}</span>`);
+        line = line.replace(negativePattern, (_, space, tag) => `${space}<span style="color: rgb(230, 59, 44);">${escapeHtml(tag)}</span>`);
+
+        return line.trim();
+    });
+
+    // Join lines with proper <br> handling
+    let finalContent = highlightedLines.join("<br>");
+
+    //console.log("MDLG: Highlighted content:", finalContent);
+    return finalContent;
+}
+
+
+
+
+    async entitiesReloadedEvent({ loadResults }) {
+        //console.log("MDLG: Entities reloaded event triggered");
+        if (loadResults.isNoteContentReloaded(this.noteId)) {
+            //console.log("MDLG: Note content reloaded, refreshing widget...");
+            //this.refresh();
+        } else {
+            //console.log("MDLG: No content reload detected.");
+        }
+    }
+    
+
+}
+
+console.log("MDLG: Starting Moodlog Highlighter Widget");
+module.exports = MoodlogHighlighterWidget;
+
+function setNoteContent(noteId, content) {
+    /*const noteElement = document.querySelector(`note-detail-readonly-text-content`);
+
+    if (noteElement) {
+        noteElement.innerHTML = content; // Update the displayed content
+        console.log(`MDLG: Updated note ${noteId} in the DOM.`);
+    } else {
+        console.warn(`MDLG: Note element with data-note-id='${noteId}' not found.`);
+    }*/
+    return api.runOnBackend((id, data) => api.getNote(id).setContent(data), [noteId, content]);
+}
diff --git a/highlight_todotxt.js b/highlight_todotxt.js
new file mode 100644
index 0000000..1f1b007
--- /dev/null
+++ b/highlight_todotxt.js
@@ -0,0 +1,157 @@
+const TPL = `<div style="padding: 10px; border-top: 1px solid var(--main-border-color); contain: none;">
+    <strong>Todo Highlighting Applied</strong>
+</div>`;
+
+class MoodlogHighlighterWidget extends api.NoteContextAwareWidget {
+    static get parentWidget() { 
+        //console.log("MDLG: Getting parent widget: center-pane");
+        return 'center-pane'; 
+    }
+
+    get position() { 
+        //console.log("MDLG: Setting position: 100");
+        return 100; 
+    }
+
+    isEnabled() {
+        const enabled = super.isEnabled() 
+            && this.note.type === 'text' 
+            && this.note.hasLabel('todotxt');
+        //console.log(`MDLG: isEnabled: ${enabled}`);
+        return enabled;
+    }
+
+    doRender() {
+        //console.log("MDLG: Rendering widget...");
+        this.$widget = $(TPL);
+        //console.log("MDLG: Widget rendered:", this.$widget);
+        return this.$widget;
+    }
+
+    async refreshWithNote(note) {
+        //console.log("MDLG: Refreshing with note:", note);
+        const { content } = await note.getNoteComplement();
+        //console.log("MDLG: Note content fetched:", content);
+
+        // Check if content is fetched properly
+        if (!content) {
+            //console.log("MDLG: No content fetched. Exiting refresh.");
+            return;
+        }
+
+        const highlightedContent = this.applyHighlighting(content);
+        //console.log("MDLG: Highlighted content:", highlightedContent);
+
+        if (highlightedContent !== content) {
+            //console.log("MDLG: Updating note content with highlighted version");
+
+            // Use the setNoteContent function to update the note content
+            try {
+                await setNoteContent(note.noteId, highlightedContent);
+                //console.log("MDLG: Content updated with highlighted version");
+            } catch (error) {
+                //console.error("MDLG: Error updating content:", error);
+            }
+        } else {
+            //console.log("MDLG: No changes to content, skipping update.");
+        }
+    }
+
+
+applyHighlighting(content) {
+    // Replace <br> with \n, then remove all other HTML tags
+    const plainTextContent = content.replace(/<br\s*\/?\>/gi, '\n').replace(/<[^>]*>/g, ' ');
+
+    // Regular expressions for Todo.txt syntax
+    const datePattern = /\b(0[1-9]|[12]\d|3[01])\/(0[1-9]|1[0-2])\/([1-2]\d{3})\b/g;
+    const completedTaskPattern = /^\s*x\s+.*/g;
+    const priorityPattern = /^(\s*\([A-E]\))\s+/;
+    const projectPattern = /(?:\s|^)(\+\S+)/g;
+    const contextPattern = /(?:\s|^)(@\S+)/g;
+    const waitPattern = /\bWAIT\b/gi;
+    const customAttributePattern = /(\s+[^\s:]+:[^\s:]+)+\s*$/g;
+    
+    function escapeHtml(str) {
+        return str.replace(/[&<>"']/g, char => ({
+            '&': '&amp;', '<': '&lt;', '>': '&gt;',
+            '"': '&quot;', "'": '&#039;'
+        })[char]);
+    }
+
+    let highlightedLines = plainTextContent.split("\n").map(line => {
+        // If line starts with 'x', make the whole line grey
+        if (/^\s*x\s+/.test(line)) {
+            return `<span style="color: grey;">${line}</span>`;
+        }
+        
+        // Highlight priority with different colours based on level
+        line = line.replace(priorityPattern, (match, priority) => {
+            const level = priority.match(/[A-E]/)?.[0];
+            const colors = {
+                'A': 'rgb(255, 0, 0)',       // Red
+                'B': 'rgb(255, 102, 0)',     // Orange
+                'C': 'rgb(255, 204, 0)',     // Yellow
+                'D': 'rgb(0, 153, 255)',     // Blue
+                'E': 'rgb(102, 204, 102)'    // Green
+            };
+            const color = colors[level] || 'black';
+            return `<span style="color: ${color}; font-weight: bold;">${escapeHtml(priority)}</span> `;
+        });
+        
+        // Highlight date
+        line = line.replace(datePattern, (match) => {
+            return `<span style="color: rgb(188, 55, 237);">${escapeHtml(match)}</span>`;
+        });
+
+        // Highlight project tags
+        line = line.replace(projectPattern, (_, tag) => `<span style="color: rgb(224, 218, 36);"> ${escapeHtml(tag)}</span>`);
+        
+        // Highlight context tags
+        line = line.replace(contextPattern, (_, tag) => `<span style="color: rgb(255, 165, 10);"> ${escapeHtml(tag)}</span>`);
+        
+        // Highlight WAIT command
+        line = line.replace(waitPattern, `<span style="color: rgb(255, 69, 0);">WAIT</span>`);
+        
+        // Highlight custom attributes
+        line = line.replace(customAttributePattern, `<span style="color: rgb(100, 149, 237);">$1</span>`);
+        
+        // Remove leading space (if any) from the beginning of the line, without affecting spaces before + or @
+        line = line.replace(/^\s+/, '');
+        
+        return line.trim();
+    });
+
+    return highlightedLines.join("<br>");
+}
+
+
+
+
+
+    async entitiesReloadedEvent({ loadResults }) {
+        //console.log("MDLG: Entities reloaded event triggered");
+        if (loadResults.isNoteContentReloaded(this.noteId)) {
+            //console.log("MDLG: Note content reloaded, refreshing widget...");
+            //this.refresh();
+        } else {
+            //console.log("MDLG: No content reload detected.");
+        }
+    }
+    
+
+}
+
+console.log("TodoHighlight: Starting todotxt Highlighter Widget");
+module.exports = MoodlogHighlighterWidget;
+
+function setNoteContent(noteId, content) {
+    /*const noteElement = document.querySelector(`note-detail-readonly-text-content`);
+
+    if (noteElement) {
+        noteElement.innerHTML = content; // Update the displayed content
+        console.log(`MDLG: Updated note ${noteId} in the DOM.`);
+    } else {
+        console.warn(`MDLG: Note element with data-note-id='${noteId}' not found.`);
+    }*/
+    return api.runOnBackend((id, data) => api.getNote(id).setContent(data), [noteId, content]);
+}
diff --git a/htb2trilium/INSTALL.md b/htb2trilium/INSTALL.md
new file mode 100644
index 0000000..da8afad
--- /dev/null
+++ b/htb2trilium/INSTALL.md
@@ -0,0 +1,41 @@
+## scripts require:
+
+```
+ > pip3 install trilium-py
+```
+
+## config.json
+- get trilium token in trilium options->ETAPI and "create new token"
+- create a new note with a title "Machines"
+- click on this note -> "note info". Note ID is there. This ID goes in "trilium_machines_htb_folder" in the config file
+- set the folder's owned attributes to:
+
+```
+#label:user=promoted,single,text #label:root=promoted,single,text #label:respect=promoted,single,text #user=0 #root=0 #respect=0 
+```
+
+ - create a note named "HTBMachineTemplate" 
+ - set it's "owned attributes" to:
+
+```
+#template #label:User=promoted,single,text #label:Root=promoted,single,text #label:Tags=promoted,single,text
+```
+
+- get this page's ID and put in "trilium_machines_template_id"
+- create "challenges" page, this page's ID goes in: trilium_challenges_folder  
+- create "Sherlocks" page, this page's ID goes in: trilium_sherlocks_folder  
+- create 2 pages "HTBChallengesTemplate" and "HTBSherlocksTemplate", both of these should have the owned attributes:
+
+```
+#template #label:Difficulty=promoted,single,text #label:Solved=promoted,single,text #label:Released=promoted,single,text 
+```
+
+- The ID's of "HTBChallengesTemplate" and "HTBSherlocksTemplate" go in the matching config values.
+
+- to get "in progress" todo colour - add to trilium demo/scripting/taskmanager/implementation/CSS:
+
+```
+ span.fancytree-node.inprogress .fancytree-title {
+    color: orange !important;
+ }
+```
\ No newline at end of file
diff --git a/htb2trilium/README.md b/htb2trilium/README.md
new file mode 100644
index 0000000..1b45ae2
--- /dev/null
+++ b/htb2trilium/README.md
@@ -0,0 +1,16 @@
+htb2trilium
+===============
+
+Pull hackthebox info and stats into personal trilium note taking software
+
+single machine view:
+
+![machine view](images/machine_view.png)
+
+overview of machines:
+
+![machines overview](images/machines_overview.png)
+
+navigation:
+
+![navigation](images/navigation.png)
\ No newline at end of file
diff --git a/htb2trilium/config.json b/htb2trilium/config.json
new file mode 100644
index 0000000..39597ce
--- /dev/null
+++ b/htb2trilium/config.json
@@ -0,0 +1,11 @@
+{
+	"htb_code": "in https://app.hackthebox.com/profile/settings click 'profile settings' and under 'app tokens' you can 'create app token' - that goes here",
+	"trilium_server_url": "https://place.to.your.trilium.server",
+	"trilium_token": "",
+	"trilium_machines_htb_folder": "",
+	"trilium_machines_template_id": "",
+	"trilium_challenges_folder": "",
+	"trilium_challenges_template_id": "",
+	"trilium_sherlocks_folder": "",
+	"trilium_sherlocks_template_id": ""
+}
\ No newline at end of file
diff --git a/htb2trilium/htb2trilium_challenges.py b/htb2trilium/htb2trilium_challenges.py
new file mode 100644
index 0000000..d0d95ca
--- /dev/null
+++ b/htb2trilium/htb2trilium_challenges.py
@@ -0,0 +1,146 @@
+import os
+import json
+import requests
+from collections import defaultdict
+from bs4 import BeautifulSoup
+from datetime import datetime, timezone
+from htb_client import HTBClient
+from trilium_py.client import ETAPI
+
+# Get the absolute path of the script's directory
+script_dir = os.path.dirname(os.path.abspath(__file__))
+
+# Construct the full path to the config.json file
+config_path = os.path.join(script_dir, 'config.json')
+
+# Load configuration from the JSON file
+with open(config_path, 'r') as f:
+    config = json.load(f)
+
+# Accessing config values
+htb_code = config['htb_code']
+trilium_server_url = config['trilium_server_url']
+trilium_token = config['trilium_token']
+trilium_challenges_folder = config['trilium_challenges_folder']
+trilium_challenges_template_id = config['trilium_challenges_template_id']
+
+def get_timestamp(machine):
+    # Parse the release date string into a datetime object
+    release_str = machine['release_date']
+    dt = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ")
+    
+    # Set the timezone to UTC
+    dt = dt.replace(tzinfo=timezone.utc)
+    
+    # Get the timestamp
+    timestamp = dt.timestamp()
+    return timestamp
+
+print("[+] connecting to HTB")
+client = HTBClient(password=htb_code)
+print("[+] connecting to trilium")
+ea = ETAPI(trilium_server_url, trilium_token)
+print("[i] version: ", ea.app_info()['appVersion'])
+
+print("[i] HTB User:", client.user['id'], "-", client.user['name'])
+
+
+print("[+] gathering challenges info")
+categories = defaultdict(list)
+challenges = client.get_all_challenges()
+challenges.sort(key=get_timestamp)
+total_completed = 0  # Variable to track total completed challenges
+
+print(f"[i] Retrieved {len(challenges)} challenges")
+
+# Group challenges by their categories
+for challenge in challenges:
+    categories[challenge['category_name']].append(challenge)
+    if challenge['is_owned']:
+        total_completed += 1  # Increment total completed if challenge is owned
+
+# Print out the grouped challenges with the number of completed and total challenges in each category
+for category, grouped_challenges in categories.items():
+    total = len(grouped_challenges)
+    completed = sum(1 for challenge in grouped_challenges if challenge['is_owned'])  # Count completed challenges
+    
+    res = ea.search_note(
+        search=f"note.title %= '{category}*'",
+        ancestorNoteId=trilium_challenges_folder,
+        ancestorDepth='eq1',
+        limit=1,
+        fastSearch=True,
+    )
+    
+    catId = ""
+    if res['results'] and res['results'][0]['title'].split(' - ')[0].strip().lower() == category.lower():
+        # page exists - lets check if the details have changed
+        ea.patch_note(noteId=res['results'][0]['noteId'], title=category+" - "+str(completed)+" / "+str(total))
+        catId = res['results'][0]['noteId']
+        print(f"[i] updated category: {category} - ({completed}/{total})")
+    else:
+        new_note = ea.create_note(
+            parentNoteId=trilium_challenges_folder,
+            type="text",
+            title=category+" - "+str(completed)+" / "+str(total),
+            content=" ",
+        )
+        catId = new_note['note']['noteId']
+        print(f"[+] created category: {catId} {category} - ({completed}/{total})")
+    
+    for challenge in grouped_challenges:
+        #print(f" - ID: {challenge['id']}, Name: {challenge['name']}, Difficulty: {challenge['difficulty']}")
+        res2 = ea.search_note(
+            search=f"{challenge['name']}",
+            ancestorNoteId=catId,
+            ancestorDepth='eq1',
+            limit=1,
+            fastSearch=True,
+        )
+        # already exists update the values
+        if res2['results'] and res2['results'][0]['title'].lower() == challenge['name'].lower():
+            print(f"[i] found ID: {challenge['id']}, Name: {challenge['name']}, Difficulty: {challenge['difficulty']}")
+            #print(f"Search response for challenge '{challenge['name']}': {res2}")
+            for attribute in res2['results'][0]['attributes']:
+                if attribute['name'] == "Difficulty":
+                    ea.patch_attribute(attributeId=attribute['attributeId'], value=challenge['difficulty'])
+                if attribute['name'] == "Released":
+                    release_str = challenge['release_date']
+                    release_date = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ")
+                    formatted_release_date = release_date.strftime("%d %B %Y")
+                    ea.patch_attribute(attributeId=attribute['attributeId'], value=formatted_release_date)
+                if attribute['name'] == "Solved":
+                    if challenge['is_owned']:
+                        ea.patch_attribute(attributeId=attribute['attributeId'], value="done")
+                    else:
+                        ea.patch_attribute(attributeId=attribute['attributeId'], value=" ")
+                if attribute['name'] == "cssClass":
+                    if challenge['is_owned']:
+                        ea.patch_attribute(attributeId=attribute['attributeId'], value="done")
+                    else:
+                        ea.patch_attribute(attributeId=attribute['attributeId'], value="todo")
+
+        else: # doesnt already exist, create page
+            release_str = challenge['release_date']
+            release_date = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ")
+            formatted_release_date = release_date.strftime("%d %B %Y")
+            new_note = ea.create_note(
+                parentNoteId=catId,
+                type="text",
+                title=challenge['name'],
+                content=" ",
+            )
+            ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="relation", name="template", value=trilium_challenges_template_id)
+            ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Difficulty", value=challenge['difficulty'])
+            ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Released", value=formatted_release_date)
+            if challenge['is_owned']:
+                ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="done")
+                ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Solved", value="done")
+            else:
+                ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="todo")
+                ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Solved", value=" ")
+
+            print(f"[+] created ID: {challenge['id']}, Name: {challenge['name']}, Difficulty: {challenge['difficulty']}")
+
+print("[+] updating folder name                                          ")
+ea.patch_note(noteId=trilium_challenges_folder,title="Challenges - "+str(total_completed)+" / "+str(len(challenges)))
diff --git a/htb2trilium/htb2trilium_machines.py b/htb2trilium/htb2trilium_machines.py
new file mode 100644
index 0000000..de8be53
--- /dev/null
+++ b/htb2trilium/htb2trilium_machines.py
@@ -0,0 +1,203 @@
+import os
+import json
+import requests
+from bs4 import BeautifulSoup
+from datetime import datetime, timezone
+from htb_client import HTBClient
+from trilium_py.client import ETAPI
+
+# Get the absolute path of the script's directory
+script_dir = os.path.dirname(os.path.abspath(__file__))
+
+# Construct the full path to the config.json file
+config_path = os.path.join(script_dir, 'config.json')
+
+# Load configuration from the JSON file
+with open(config_path, 'r') as f:
+    config = json.load(f)
+
+# Accessing config values
+htb_code = config['htb_code']
+trilium_server_url = config['trilium_server_url']
+trilium_token = config['trilium_token']
+trilium_machines_htb_folder = config['trilium_machines_htb_folder']
+trilium_machines_template_id = config['trilium_machines_template_id']
+
+def generate_newpage(machine):
+	no = "<span style=\"color:hsl(0,75%,60%);\">No</span>"
+	yes = "<span style=\"color:hsl(120,75%,60%);\">Yes</span>"
+	user_colour = no
+	if machine['authUserInUserOwns']:
+		user_colour = yes
+	root_colour = no
+	if machine['authUserInRootOwns']:
+		root_colour = yes
+
+	status = "Retired"
+	if machine['active']:
+		status = "Active"
+
+	release_str = machine['release']
+	release_date = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ")
+	formatted_release_date = release_date.strftime("%d %B %Y")
+
+	html = """
+	<figure class="image image-style-align-left image_resized" style="width:12.28%;">
+	  <img src="https://www.hackthebox.com{avatar}">
+	</figure>
+	<p>
+	  <strong>OS:</strong> {os}<br>
+	  <strong>Difficulty:</strong> {difficultyText} <br>
+	  <strong>Rating:</strong> {rating} / 5<br>
+	  <strong>Points:</strong> {points}<br>
+	  <strong>User / Root: </strong> {user_colour} / {root_colour}<br>
+	  <strong>Released:</strong> {release_date}<br>
+	  <strong>State:</strong> {status}
+	</p>
+	<hr>
+	<h2>Notes</h2>
+	<p>&nbsp;</p>
+	""".format(
+		os=machine['os'],
+		difficultyText=machine['difficultyText'],
+		rating=machine['star'],
+		points=machine['points'],
+		release_date=formatted_release_date,
+		user_colour=user_colour,
+		root_colour=root_colour,
+		status = status,
+		avatar = machine['avatar'],
+	)
+
+	return html
+
+def get_timestamp(machine):
+    # Parse the release date string into a datetime object
+    release_str = machine['release']
+    dt = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ")
+    
+    # Set the timezone to UTC
+    dt = dt.replace(tzinfo=timezone.utc)
+    
+    # Get the timestamp
+    timestamp = dt.timestamp()
+    return timestamp
+
+print("[+] connecting to HTB")
+client = HTBClient(password=htb_code)
+print("[+] connecting to trilium")
+ea = ETAPI(trilium_server_url, trilium_token)
+print("[i] version: ", ea.app_info()['appVersion'])
+
+print("[i] HTB User:", client.user['id'], "-", client.user['name'])
+print("[i] user owns:", client.user['user_owns'], "| Root owns:", client.user['root_owns'], "| Respect:", client.user['respects'])
+
+master_folder = ea.get_note(trilium_machines_htb_folder)
+for attribute in master_folder['attributes']:
+	if attribute['name'] == "user":
+		if attribute['value'] != str(client.user['user_owns']):
+			print("[+] updating user owns (folder attribute)")
+			ea.patch_attribute(attributeId=attribute['attributeId'], value=str(client.user['user_owns']))
+	if attribute['name'] == "root":
+		if attribute['value'] != str(client.user['root_owns']):
+			print("[+] updating root owns (folder attribute)")
+			ea.patch_attribute(attributeId=attribute['attributeId'], value=str(client.user['root_owns']))
+	if attribute['name'] == "respect":
+		if attribute['value'] != str(client.user['respects']):
+			print("[+] updating respect (folder attribute)")
+			ea.patch_attribute(attributeId=attribute['attributeId'], value=str(client.user['respects']))
+
+print("[+] gathering machines info")
+machines = client.get_all_machines()
+machines.sort(key=get_timestamp)
+
+print(f"[i] Retrieved {len(machines)} machines")
+#for machine in machines:
+#    print(f" - ID: {machine['id']}, Name: {machine['name']}, OS: {machine['os']}, Difficulty: {machine['difficultyText']}")
+
+machine_count = 0
+completed_count = 0
+for machine in machines:
+	machine_count += 1
+	print('processing: ',machine_count, "/", len(machines), "("+machine['name']+")               " , end='\r')
+
+	res = ea.search_note(
+		search="\""+machine['name']+"\"",
+		ancestorNoteId=trilium_machines_htb_folder,
+		ancestorDepth='eq1',
+		limit=1,
+		fastSearch=True,
+	)
+
+	if res['results'] and machine['name'] == res['results'][0]['title']:
+		# page exists - lets check if the details have changed
+
+		current_html = ea.get_note_content(noteId=res['results'][0]['noteId'])
+		current_soup = BeautifulSoup(current_html, 'html.parser')
+		current_paragraph = current_soup.find_all('p')[0].text
+
+		new_html = generate_newpage(machine)
+		new_soup = BeautifulSoup(new_html, 'html.parser')
+		new_paragraph = new_soup.find_all('p')[0].text
+
+		# current page contains first paragraph of "blank" (useful for when it doesnt create or find the note properly.. shouldnt get here)
+		if current_paragraph == "blank":
+			ea.update_note_content(noteId=res['results'][0]['noteId'], content=new_html)
+			ea.create_attribute(attributeId=None, isInheritable=False, noteId=res['results'][0]['noteId'], type="relation", name="template", value=trilium_machines_htb_folder)
+			if machine['authUserInUserOwns'] and machine['authUserInRootOwns']:
+				ea.create_attribute(attributeId=None, isInheritable=False, noteId=res['results'][0]['noteId'], type="label", name="cssClass", value="done")
+			else:
+				if machine['authUserInUserOwns'] or machine['authUserInRootOwns']:
+					ea.create_attribute(attributeId=None, isInheritable=False, noteId=res['results'][0]['noteId'], type="label", name="cssClass", value="inprogress")
+				else:
+					ea.create_attribute(attributeId=None, isInheritable=False, noteId=res['results'][0]['noteId'], type="label", name="cssClass", value="todo")
+			# re-get the current content
+			current_html = ea.get_note_content(noteId=res['results'][0]['noteId'])
+			current_soup = BeautifulSoup(current_html, 'html.parser')
+
+		if current_paragraph != new_paragraph:
+
+			# details have updated!
+			print("[+] updating page:",machine['name'], "-> "+res['results'][0]['title']+"                 ")
+			replacement = current_soup.find('p')
+			replacement.replace_with( new_soup.find_all('p')[0] )
+			ea.update_note_content(noteId=res['results'][0]['noteId'], content=current_soup)
+
+			# now to update the label
+			for attribute in res['results'][0]['attributes']:
+				if attribute['name'] == "cssClass":
+					if machine['authUserInUserOwns'] and machine['authUserInRootOwns']:
+						ea.patch_attribute(attributeId=attribute['attributeId'], value="done")
+					else:
+						if machine['authUserInUserOwns'] or machine['authUserInRootOwns']:
+							ea.patch_attribute(attributeId=attribute['attributeId'], value="inprogress")
+						else:
+							ea.patch_attribute(attributeId=attribute['attributeId'], value="todo")
+							
+	else:
+		# title does not exist - create the note
+		html = generate_newpage(machine)
+		new_note = ea.create_note(
+			parentNoteId=trilium_machines_htb_folder,
+			type="text",
+			title=machine['name'],
+			content=html,
+		)
+		print("[+] created note:", machine['name'], "                        ")
+
+		ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="relation", name="template", value=trilium_machines_htb_folder)
+		if machine['authUserInUserOwns'] and machine['authUserInRootOwns']:
+			ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="done")
+		else:
+			if machine['authUserInUserOwns'] or machine['authUserInRootOwns']:
+				ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="inprogress")
+			else:
+				ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="todo")
+
+	if machine['authUserInUserOwns'] and machine['authUserInRootOwns']:
+			completed_count += 1
+
+print("[+] updating folder name                                          ")
+ea.patch_note(noteId=trilium_machines_htb_folder,title="Machines - "+str(completed_count)+" / "+str(len(machines)))
+
+print("[=] processed", machine_count)
\ No newline at end of file
diff --git a/htb2trilium/htb2trilium_sherlocks.py b/htb2trilium/htb2trilium_sherlocks.py
new file mode 100644
index 0000000..1e32e59
--- /dev/null
+++ b/htb2trilium/htb2trilium_sherlocks.py
@@ -0,0 +1,151 @@
+import os
+import json
+import requests
+from collections import defaultdict
+from bs4 import BeautifulSoup
+from datetime import datetime, timezone
+from htb_client import HTBClient
+from trilium_py.client import ETAPI
+
+# Get the absolute path of the script's directory
+script_dir = os.path.dirname(os.path.abspath(__file__))
+
+# Construct the full path to the config.json file
+config_path = os.path.join(script_dir, 'config.json')
+
+# Load configuration from the JSON file
+with open(config_path, 'r') as f:
+    config = json.load(f)
+
+# Accessing config values
+htb_code = config['htb_code']
+trilium_server_url = config['trilium_server_url']
+trilium_token = config['trilium_token']
+trilium_sherlocks_folder = config['trilium_sherlocks_folder']
+trilium_sherlocks_template_id = config['trilium_sherlocks_template_id']
+
+def get_timestamp(machine):
+    # Parse the release date string into a datetime object
+    release_str = machine['release_date']
+    dt = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ")
+    
+    # Set the timezone to UTC
+    dt = dt.replace(tzinfo=timezone.utc)
+    
+    # Get the timestamp
+    timestamp = dt.timestamp()
+    return timestamp
+
+print("[+] connecting to HTB")
+client = HTBClient(password=htb_code)
+print("[+] connecting to trilium")
+ea = ETAPI(trilium_server_url, trilium_token)
+print("[i] version: ", ea.app_info()['appVersion'])
+
+print("[i] HTB User:", client.user['id'], "-", client.user['name'])
+
+
+print("[+] gathering sherlocks info")
+categories = defaultdict(list)
+sherlocks = client.get_all_sherlocks()
+sherlocks.sort(key=get_timestamp)
+total_completed = 0  # Variable to track total completed sherlocks
+
+print(f"[i] Retrieved {len(sherlocks)} sherlocks")
+
+# Group sherlocks by their categories
+for sherlock in sherlocks:
+    categories[sherlock['category_name']].append(sherlock)
+    if sherlock['progress'] == 100:
+        total_completed += 1  # Increment total completed if challenge is owned
+
+# Print out the grouped sherlocks with the number of completed and total sherlocks in each category
+for category, grouped_sherlocks in categories.items():
+    total = len(grouped_sherlocks)
+    completed = sum(1 for sherlock in grouped_sherlocks if sherlock['progress'] == 100)  # Count completed challenges
+    
+    res = ea.search_note(
+        search=f"note.title %= '{category}*'",
+        ancestorNoteId=trilium_sherlocks_folder,
+        ancestorDepth='eq1',
+        limit=1,
+        fastSearch=True,
+    )
+    
+    catId = ""
+    if res['results'] and res['results'][0]['title'].split(' - ')[0].strip().lower() == category.lower():
+	# page exists - lets check if the details have changed
+        ea.patch_note(noteId=res['results'][0]['noteId'], title=category+" - "+str(completed)+" / "+str(total))
+        catId = res['results'][0]['noteId']
+        print(f"[i] updated category: {category} - ({completed}/{total})")
+    else:
+        new_note = ea.create_note(
+            parentNoteId=trilium_sherlocks_folder,
+            type="text",
+            title=category+" - "+str(completed)+" / "+str(total),
+            content=" ",
+        )
+        catId = new_note['note']['noteId']
+        print(f"[+] created category: {catId} {category} - ({completed}/{total})")
+    
+    for sherlock in grouped_sherlocks:
+        #print(f" - ID: {challenge['id']}, Name: {challenge['name']}, Difficulty: {challenge['difficulty']}")
+        res2 = ea.search_note(
+            search=f"{sherlock['name']}",
+            ancestorNoteId=catId,
+            ancestorDepth='eq1',
+            limit=1,
+            fastSearch=True,
+        )
+        # already exists update the values
+        if res2['results'] and res2['results'][0]['title'].lower() == sherlock['name'].lower():
+            print(f"[i] found ID: {sherlock['id']}, Name: {sherlock['name']}, Difficulty: {sherlock['difficulty']}")
+    
+            for attribute in res2['results'][0]['attributes']:
+                if attribute['name'] == "Difficulty":
+                    ea.patch_attribute(attributeId=attribute['attributeId'], value=sherlock['difficulty'])
+        
+                if attribute['name'] == "Released":
+                    release_str = sherlock['release_date']
+                    release_date = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ")
+                    formatted_release_date = release_date.strftime("%d %B %Y")
+                    ea.patch_attribute(attributeId=attribute['attributeId'], value=formatted_release_date)
+        
+                if attribute['name'] == "Solved":
+                    ea.patch_attribute(attributeId=attribute['attributeId'], value=str(sherlock['progress']))
+        
+                if attribute['name'] == "cssClass":
+                    # Determine the cssClass value based on the progress
+                    if sherlock['progress'] == 0:
+                        ea.patch_attribute(attributeId=attribute['attributeId'], value="todo")
+                    elif sherlock['progress'] == 100:
+                        ea.patch_attribute(attributeId=attribute['attributeId'], value="done")
+                    elif 0 < sherlock['progress'] < 100:
+                        ea.patch_attribute(attributeId=attribute['attributeId'], value="inprogress")
+
+
+        else: # doesnt already exist, create page
+            release_str = sherlock['release_date']
+            release_date = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ")
+            formatted_release_date = release_date.strftime("%d %B %Y")
+            new_note = ea.create_note(
+                parentNoteId=catId,
+                type="text",
+                title=sherlock['name'],
+                content=" ",
+            )
+            ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="relation", name="template", value=trilium_sherlocks_template_id)
+            ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Difficulty", value=sherlock['difficulty'])
+            ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Released", value=formatted_release_date)
+            ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Solved", value=str(sherlock['progress']))
+            if sherlock['progress'] == 0:
+                ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="todo")
+            elif sherlock['progress'] == 100:
+                ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="done")
+            elif 0 < sherlock['progress'] < 100:
+                ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="inprogress")
+
+            print(f"[+] created ID: {sherlock['id']}, Name: {sherlock['name']}, Difficulty: {sherlock['difficulty']}")
+
+print("[+] updating folder name                                          ")
+ea.patch_note(noteId=trilium_sherlocks_folder,title="Sherlocks - "+str(total_completed)+" / "+str(len(sherlocks)))

diff --git a/README.md b/README.md
index f623b41..fefccb6 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,58 @@
 TriliumScripts
 ===============
 
-My collection of scripts for use with trilium
\ No newline at end of file
+Collection of scripts for use with trilium
+
+## syntax highlighting
+
+**highlight_moodlog.js** & **highlight_todo.js** - Both of these will apply appropriate syntaxy highlighting to the specific note.
+
+### installation
+
+1) add the code as the notes contents.
+
+2) set "note type" to "JS frontend"
+
+3) add an "owned attribute" of "#widget"
+
+### use
+
+on the note you want highlighting set the "owned attributes" to "#todotxt #hideHighlightWidget" or "#moodlog #hideHighlightWidget" depending on which syntax type you want to highlight
+
+## bidirectional sync
+
+**trilium_sync.php** - script to keep a folder of local text files and a trilium note in sync.
+
+### installation
+
+Edit the config values at the top of the script to match your trilium setup. Run it manually to test it is working as expected. If it is create a cron job.
+
+### example use
+
+```
+$> php trilium_sync.php 
+[i] checking connections
+[i] version: x.xx.x
+[i] gathering info
+
+- Folders -
+Folder/Path              Folder Name              Parent       noteId       Mod Trl    Mod Dsk    Status         
+Synced/                  Synced                   root         j0DRZhIzV97W 1742502936 1742507367 Exists
+Synced/BothPlaces/       BothPlaces               j0DRZhIzV97W AQb3Dg0yZHSu 1742507075 1742507200 Exists
+Synced/[del] tobe del/   [del] tobe del           j0DRZhIzV97W j0xUs1FdZL1Y 1742507321            Missing_Disk   
+Synced/AddToTrilium/     AddToTrilium                                                  1742507383 Missing_Trilium
+Synced/tobe deleted/     tobe deleted                                                  1742507200 Missing_Trilium
+- Files -
+Folder/Path              Filename                 Parent       noteId       Mod Trl    Mod Dsk    Hash  Status         
+Synced/BothPlaces/       UpdateDiskFile           AQb3Dg0yZHSu IwUZLwWdbiWG 1742507314 1742507199 Diff  Update_Disk    
+Synced/BothPlaces/       UpdateTriliumFile        AQb3Dg0yZHSu 7pMPgsb9AVRf 1742507130 1742507345 Diff  Update_Trilium 
+Synced/[del] tobe del/   gone_also.txt            j0xUs1FdZL1Y 4wOoHDy07csr 1742507178            N/A   Missing_Disk   
+Synced/AddToTrilium/     fromDiskToTrilium.txt                                         1742507394 N/A   Missing_Trilium
+Synced/tobe deleted/     gone_also.txt                                                 1742507200 N/A   Missing_Trilium
+
+[-] deleting disk folder: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/tobe deleted/
+[+] creating trilium folder: Synced/AddToTrilium/
+[+] creating trilium note: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/AddToTrilium/fromDiskToTrilium.txt
+[+] updating file on disk: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/BothPlaces/UpdateDiskFile
+[+] updating file on trilium: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/BothPlaces/UpdateTriliumFile
+```
\ No newline at end of file
diff --git a/highlight_moodlog.js b/highlight_moodlog.js
new file mode 100644
index 0000000..be4c283
--- /dev/null
+++ b/highlight_moodlog.js
@@ -0,0 +1,165 @@
+const TPL = `<div style="padding: 10px; border-top: 1px solid var(--main-border-color); contain: none;">
+    <strong>Moodlog Highlighting Applied</strong>
+</div>`;
+
+class MoodlogHighlighterWidget extends api.NoteContextAwareWidget {
+    static get parentWidget() { 
+        //console.log("MDLG: Getting parent widget: center-pane");
+        return 'center-pane'; 
+    }
+
+    get position() { 
+        //console.log("MDLG: Setting position: 100");
+        return 100; 
+    }
+
+    isEnabled() {
+        const enabled = super.isEnabled() 
+            && this.note.type === 'text' 
+            && this.note.hasLabel('moodlog');
+        //console.log(`MDLG: isEnabled: ${enabled}`);
+        return enabled;
+    }
+
+    doRender() {
+        //console.log("MDLG: Rendering widget...");
+        this.$widget = $(TPL);
+        //console.log("MDLG: Widget rendered:", this.$widget);
+        return this.$widget;
+    }
+
+    async refreshWithNote(note) {
+        //console.log("MDLG: Refreshing with note:", note);
+        const { content } = await note.getNoteComplement();
+        //console.log("MDLG: Note content fetched:", content);
+
+        // Check if content is fetched properly
+        if (!content) {
+            //console.log("MDLG: No content fetched. Exiting refresh.");
+            return;
+        }
+
+        const highlightedContent = this.applyHighlighting(content);
+        //console.log("MDLG: Highlighted content:", highlightedContent);
+
+        if (highlightedContent !== content) {
+            //console.log("MDLG: Updating note content with highlighted version");
+
+            // Use the setNoteContent function to update the note content
+            try {
+                await setNoteContent(note.noteId, highlightedContent);
+                //console.log("MDLG: Content updated with highlighted version");
+            } catch (error) {
+                //console.error("MDLG: Error updating content:", error);
+            }
+        } else {
+            //console.log("MDLG: No changes to content, skipping update.");
+        }
+    }
+
+
+ applyHighlighting(content) {
+    //console.log("MDLG: Applying highlighting to content...");
+
+    // Replace <br> with \n, then remove all other HTML tags
+    const plainTextContent = content.replace(/<br\s*\/?>/gi, '\n').replace(/<[^>]*>/g, ' ');
+    //console.log("MDLG: Stripped content:", plainTextContent);
+
+    // Combined regex for date & time
+    const dateTimePattern = /([1-2]\d{3}[-/.](0?[1-9]|1[0-2])[-/.](0[1-9]|[12]\d|3[01]))\s+(\d{1,2}:\d{2})/g;
+    const projectPattern = /\[\S+?\]/g;
+    const contextPattern = /(\s|^)(@\S+)/g;
+    const hashtagPattern = /(\s|^)(#\S+)/g;
+    const positivePattern = /(\s|^)(\+\S+)/g;
+    const negativePattern = /(\s|^)(-\S+)/g;
+
+    // Escape HTML characters to prevent issues with replacements
+    function escapeHtml(str) {
+        return str.replace(/[&<>"']/g, function (char) {
+            return {
+                '&': '&amp;',
+                '<': '&lt;',
+                '>': '&gt;',
+                '"': '&quot;',
+                "'": '&#039;'
+            }[char];
+        });
+    }
+
+      // Helper function to map the number to a colour based on the gradient
+    function getProjectColour(number) {
+        const minColor = { r: 107, g: 109, b: 229 };  // #6b6de5
+        const maxColor = { r: 70, g: 226, b: 96 };   // #46e260
+
+        const ratio = number / 9; // since we are working with 0-9
+
+        // Interpolate the RGB values between minColor and maxColor
+        const r = Math.round(minColor.r + ratio * (maxColor.r - minColor.r));
+        const g = Math.round(minColor.g + ratio * (maxColor.g - minColor.g));
+        const b = Math.round(minColor.b + ratio * (maxColor.b - minColor.b));
+
+        return `rgb(${r}, ${g}, ${b})`;
+    }
+     
+    // Process each line of the plain text content
+    let highlightedLines = plainTextContent.split("\n").map(line => {
+        //console.log("MDLG: Processing line:", line);
+
+        // Apply combined date & time highlight
+        line = line.replace(dateTimePattern, `<span style="color: rgb(188, 55, 237);">$1</span> <span style="color: rgb(188, 55, 237);">$4</span>`);
+
+        // Apply other highlights
+        line = line.replace(projectPattern, (match) => {
+            // Extract the number inside square brackets
+            const number = parseInt(match.match(/\d+/)[0], 10);
+            const colour = getProjectColour(number);
+            return `<span style="color: ${colour}; font-weight: bold;">${escapeHtml(match)}</span>`;
+        });
+        
+        // Apply other highlights
+        //line = line.replace(projectPattern, `<span style="color: rgb(224, 218, 36); font-weight: bold;">$&</span>`);
+        line = line.replace(contextPattern, (_, space, tag) => `${space}<span style="color: rgb(255 165 10);">${escapeHtml(tag)}</span>`);
+        line = line.replace(hashtagPattern, (_, space, tag) => `${space}<span style="color: rgb(41, 229, 242);">${escapeHtml(tag)}</span>`);
+        line = line.replace(positivePattern, (_, space, tag) => `${space}<span style="color: rgb(44, 227, 34);">${escapeHtml(tag)}</span>`);
+        line = line.replace(negativePattern, (_, space, tag) => `${space}<span style="color: rgb(230, 59, 44);">${escapeHtml(tag)}</span>`);
+
+        return line.trim();
+    });
+
+    // Join lines with proper <br> handling
+    let finalContent = highlightedLines.join("<br>");
+
+    //console.log("MDLG: Highlighted content:", finalContent);
+    return finalContent;
+}
+
+
+
+
+    async entitiesReloadedEvent({ loadResults }) {
+        //console.log("MDLG: Entities reloaded event triggered");
+        if (loadResults.isNoteContentReloaded(this.noteId)) {
+            //console.log("MDLG: Note content reloaded, refreshing widget...");
+            //this.refresh();
+        } else {
+            //console.log("MDLG: No content reload detected.");
+        }
+    }
+    
+
+}
+
+console.log("MDLG: Starting Moodlog Highlighter Widget");
+module.exports = MoodlogHighlighterWidget;
+
+function setNoteContent(noteId, content) {
+    /*const noteElement = document.querySelector(`note-detail-readonly-text-content`);
+
+    if (noteElement) {
+        noteElement.innerHTML = content; // Update the displayed content
+        console.log(`MDLG: Updated note ${noteId} in the DOM.`);
+    } else {
+        console.warn(`MDLG: Note element with data-note-id='${noteId}' not found.`);
+    }*/
+    return api.runOnBackend((id, data) => api.getNote(id).setContent(data), [noteId, content]);
+}
diff --git a/highlight_todotxt.js b/highlight_todotxt.js
new file mode 100644
index 0000000..1f1b007
--- /dev/null
+++ b/highlight_todotxt.js
@@ -0,0 +1,157 @@
+const TPL = `<div style="padding: 10px; border-top: 1px solid var(--main-border-color); contain: none;">
+    <strong>Todo Highlighting Applied</strong>
+</div>`;
+
+class MoodlogHighlighterWidget extends api.NoteContextAwareWidget {
+    static get parentWidget() { 
+        //console.log("MDLG: Getting parent widget: center-pane");
+        return 'center-pane'; 
+    }
+
+    get position() { 
+        //console.log("MDLG: Setting position: 100");
+        return 100; 
+    }
+
+    isEnabled() {
+        const enabled = super.isEnabled() 
+            && this.note.type === 'text' 
+            && this.note.hasLabel('todotxt');
+        //console.log(`MDLG: isEnabled: ${enabled}`);
+        return enabled;
+    }
+
+    doRender() {
+        //console.log("MDLG: Rendering widget...");
+        this.$widget = $(TPL);
+        //console.log("MDLG: Widget rendered:", this.$widget);
+        return this.$widget;
+    }
+
+    async refreshWithNote(note) {
+        //console.log("MDLG: Refreshing with note:", note);
+        const { content } = await note.getNoteComplement();
+        //console.log("MDLG: Note content fetched:", content);
+
+        // Check if content is fetched properly
+        if (!content) {
+            //console.log("MDLG: No content fetched. Exiting refresh.");
+            return;
+        }
+
+        const highlightedContent = this.applyHighlighting(content);
+        //console.log("MDLG: Highlighted content:", highlightedContent);
+
+        if (highlightedContent !== content) {
+            //console.log("MDLG: Updating note content with highlighted version");
+
+            // Use the setNoteContent function to update the note content
+            try {
+                await setNoteContent(note.noteId, highlightedContent);
+                //console.log("MDLG: Content updated with highlighted version");
+            } catch (error) {
+                //console.error("MDLG: Error updating content:", error);
+            }
+        } else {
+            //console.log("MDLG: No changes to content, skipping update.");
+        }
+    }
+
+
+applyHighlighting(content) {
+    // Replace <br> with \n, then remove all other HTML tags
+    const plainTextContent = content.replace(/<br\s*\/?\>/gi, '\n').replace(/<[^>]*>/g, ' ');
+
+    // Regular expressions for Todo.txt syntax
+    const datePattern = /\b(0[1-9]|[12]\d|3[01])\/(0[1-9]|1[0-2])\/([1-2]\d{3})\b/g;
+    const completedTaskPattern = /^\s*x\s+.*/g;
+    const priorityPattern = /^(\s*\([A-E]\))\s+/;
+    const projectPattern = /(?:\s|^)(\+\S+)/g;
+    const contextPattern = /(?:\s|^)(@\S+)/g;
+    const waitPattern = /\bWAIT\b/gi;
+    const customAttributePattern = /(\s+[^\s:]+:[^\s:]+)+\s*$/g;
+    
+    function escapeHtml(str) {
+        return str.replace(/[&<>"']/g, char => ({
+            '&': '&amp;', '<': '&lt;', '>': '&gt;',
+            '"': '&quot;', "'": '&#039;'
+        })[char]);
+    }
+
+    let highlightedLines = plainTextContent.split("\n").map(line => {
+        // If line starts with 'x', make the whole line grey
+        if (/^\s*x\s+/.test(line)) {
+            return `<span style="color: grey;">${line}</span>`;
+        }
+        
+        // Highlight priority with different colours based on level
+        line = line.replace(priorityPattern, (match, priority) => {
+            const level = priority.match(/[A-E]/)?.[0];
+            const colors = {
+                'A': 'rgb(255, 0, 0)',       // Red
+                'B': 'rgb(255, 102, 0)',     // Orange
+                'C': 'rgb(255, 204, 0)',     // Yellow
+                'D': 'rgb(0, 153, 255)',     // Blue
+                'E': 'rgb(102, 204, 102)'    // Green
+            };
+            const color = colors[level] || 'black';
+            return `<span style="color: ${color}; font-weight: bold;">${escapeHtml(priority)}</span> `;
+        });
+        
+        // Highlight date
+        line = line.replace(datePattern, (match) => {
+            return `<span style="color: rgb(188, 55, 237);">${escapeHtml(match)}</span>`;
+        });
+
+        // Highlight project tags
+        line = line.replace(projectPattern, (_, tag) => `<span style="color: rgb(224, 218, 36);"> ${escapeHtml(tag)}</span>`);
+        
+        // Highlight context tags
+        line = line.replace(contextPattern, (_, tag) => `<span style="color: rgb(255, 165, 10);"> ${escapeHtml(tag)}</span>`);
+        
+        // Highlight WAIT command
+        line = line.replace(waitPattern, `<span style="color: rgb(255, 69, 0);">WAIT</span>`);
+        
+        // Highlight custom attributes
+        line = line.replace(customAttributePattern, `<span style="color: rgb(100, 149, 237);">$1</span>`);
+        
+        // Remove leading space (if any) from the beginning of the line, without affecting spaces before + or @
+        line = line.replace(/^\s+/, '');
+        
+        return line.trim();
+    });
+
+    return highlightedLines.join("<br>");
+}
+
+
+
+
+
+    async entitiesReloadedEvent({ loadResults }) {
+        //console.log("MDLG: Entities reloaded event triggered");
+        if (loadResults.isNoteContentReloaded(this.noteId)) {
+            //console.log("MDLG: Note content reloaded, refreshing widget...");
+            //this.refresh();
+        } else {
+            //console.log("MDLG: No content reload detected.");
+        }
+    }
+    
+
+}
+
+console.log("TodoHighlight: Starting todotxt Highlighter Widget");
+module.exports = MoodlogHighlighterWidget;
+
+function setNoteContent(noteId, content) {
+    /*const noteElement = document.querySelector(`note-detail-readonly-text-content`);
+
+    if (noteElement) {
+        noteElement.innerHTML = content; // Update the displayed content
+        console.log(`MDLG: Updated note ${noteId} in the DOM.`);
+    } else {
+        console.warn(`MDLG: Note element with data-note-id='${noteId}' not found.`);
+    }*/
+    return api.runOnBackend((id, data) => api.getNote(id).setContent(data), [noteId, content]);
+}
diff --git a/htb2trilium/INSTALL.md b/htb2trilium/INSTALL.md
new file mode 100644
index 0000000..da8afad
--- /dev/null
+++ b/htb2trilium/INSTALL.md
@@ -0,0 +1,41 @@
+## scripts require:
+
+```
+ > pip3 install trilium-py
+```
+
+## config.json
+- get trilium token in trilium options->ETAPI and "create new token"
+- create a new note with a title "Machines"
+- click on this note -> "note info". Note ID is there. This ID goes in "trilium_machines_htb_folder" in the config file
+- set the folder's owned attributes to:
+
+```
+#label:user=promoted,single,text #label:root=promoted,single,text #label:respect=promoted,single,text #user=0 #root=0 #respect=0 
+```
+
+ - create a note named "HTBMachineTemplate" 
+ - set it's "owned attributes" to:
+
+```
+#template #label:User=promoted,single,text #label:Root=promoted,single,text #label:Tags=promoted,single,text
+```
+
+- get this page's ID and put in "trilium_machines_template_id"
+- create "challenges" page, this page's ID goes in: trilium_challenges_folder  
+- create "Sherlocks" page, this page's ID goes in: trilium_sherlocks_folder  
+- create 2 pages "HTBChallengesTemplate" and "HTBSherlocksTemplate", both of these should have the owned attributes:
+
+```
+#template #label:Difficulty=promoted,single,text #label:Solved=promoted,single,text #label:Released=promoted,single,text 
+```
+
+- The ID's of "HTBChallengesTemplate" and "HTBSherlocksTemplate" go in the matching config values.
+
+- to get "in progress" todo colour - add to trilium demo/scripting/taskmanager/implementation/CSS:
+
+```
+ span.fancytree-node.inprogress .fancytree-title {
+    color: orange !important;
+ }
+```
\ No newline at end of file
diff --git a/htb2trilium/README.md b/htb2trilium/README.md
new file mode 100644
index 0000000..1b45ae2
--- /dev/null
+++ b/htb2trilium/README.md
@@ -0,0 +1,16 @@
+htb2trilium
+===============
+
+Pull hackthebox info and stats into personal trilium note taking software
+
+single machine view:
+
+![machine view](images/machine_view.png)
+
+overview of machines:
+
+![machines overview](images/machines_overview.png)
+
+navigation:
+
+![navigation](images/navigation.png)
\ No newline at end of file
diff --git a/htb2trilium/config.json b/htb2trilium/config.json
new file mode 100644
index 0000000..39597ce
--- /dev/null
+++ b/htb2trilium/config.json
@@ -0,0 +1,11 @@
+{
+	"htb_code": "in https://app.hackthebox.com/profile/settings click 'profile settings' and under 'app tokens' you can 'create app token' - that goes here",
+	"trilium_server_url": "https://place.to.your.trilium.server",
+	"trilium_token": "",
+	"trilium_machines_htb_folder": "",
+	"trilium_machines_template_id": "",
+	"trilium_challenges_folder": "",
+	"trilium_challenges_template_id": "",
+	"trilium_sherlocks_folder": "",
+	"trilium_sherlocks_template_id": ""
+}
\ No newline at end of file
diff --git a/htb2trilium/htb2trilium_challenges.py b/htb2trilium/htb2trilium_challenges.py
new file mode 100644
index 0000000..d0d95ca
--- /dev/null
+++ b/htb2trilium/htb2trilium_challenges.py
@@ -0,0 +1,146 @@
+import os
+import json
+import requests
+from collections import defaultdict
+from bs4 import BeautifulSoup
+from datetime import datetime, timezone
+from htb_client import HTBClient
+from trilium_py.client import ETAPI
+
+# Get the absolute path of the script's directory
+script_dir = os.path.dirname(os.path.abspath(__file__))
+
+# Construct the full path to the config.json file
+config_path = os.path.join(script_dir, 'config.json')
+
+# Load configuration from the JSON file
+with open(config_path, 'r') as f:
+    config = json.load(f)
+
+# Accessing config values
+htb_code = config['htb_code']
+trilium_server_url = config['trilium_server_url']
+trilium_token = config['trilium_token']
+trilium_challenges_folder = config['trilium_challenges_folder']
+trilium_challenges_template_id = config['trilium_challenges_template_id']
+
+def get_timestamp(machine):
+    # Parse the release date string into a datetime object
+    release_str = machine['release_date']
+    dt = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ")
+    
+    # Set the timezone to UTC
+    dt = dt.replace(tzinfo=timezone.utc)
+    
+    # Get the timestamp
+    timestamp = dt.timestamp()
+    return timestamp
+
+print("[+] connecting to HTB")
+client = HTBClient(password=htb_code)
+print("[+] connecting to trilium")
+ea = ETAPI(trilium_server_url, trilium_token)
+print("[i] version: ", ea.app_info()['appVersion'])
+
+print("[i] HTB User:", client.user['id'], "-", client.user['name'])
+
+
+print("[+] gathering challenges info")
+categories = defaultdict(list)
+challenges = client.get_all_challenges()
+challenges.sort(key=get_timestamp)
+total_completed = 0  # Variable to track total completed challenges
+
+print(f"[i] Retrieved {len(challenges)} challenges")
+
+# Group challenges by their categories
+for challenge in challenges:
+    categories[challenge['category_name']].append(challenge)
+    if challenge['is_owned']:
+        total_completed += 1  # Increment total completed if challenge is owned
+
+# Print out the grouped challenges with the number of completed and total challenges in each category
+for category, grouped_challenges in categories.items():
+    total = len(grouped_challenges)
+    completed = sum(1 for challenge in grouped_challenges if challenge['is_owned'])  # Count completed challenges
+    
+    res = ea.search_note(
+        search=f"note.title %= '{category}*'",
+        ancestorNoteId=trilium_challenges_folder,
+        ancestorDepth='eq1',
+        limit=1,
+        fastSearch=True,
+    )
+    
+    catId = ""
+    if res['results'] and res['results'][0]['title'].split(' - ')[0].strip().lower() == category.lower():
+        # page exists - lets check if the details have changed
+        ea.patch_note(noteId=res['results'][0]['noteId'], title=category+" - "+str(completed)+" / "+str(total))
+        catId = res['results'][0]['noteId']
+        print(f"[i] updated category: {category} - ({completed}/{total})")
+    else:
+        new_note = ea.create_note(
+            parentNoteId=trilium_challenges_folder,
+            type="text",
+            title=category+" - "+str(completed)+" / "+str(total),
+            content=" ",
+        )
+        catId = new_note['note']['noteId']
+        print(f"[+] created category: {catId} {category} - ({completed}/{total})")
+    
+    for challenge in grouped_challenges:
+        #print(f" - ID: {challenge['id']}, Name: {challenge['name']}, Difficulty: {challenge['difficulty']}")
+        res2 = ea.search_note(
+            search=f"{challenge['name']}",
+            ancestorNoteId=catId,
+            ancestorDepth='eq1',
+            limit=1,
+            fastSearch=True,
+        )
+        # already exists update the values
+        if res2['results'] and res2['results'][0]['title'].lower() == challenge['name'].lower():
+            print(f"[i] found ID: {challenge['id']}, Name: {challenge['name']}, Difficulty: {challenge['difficulty']}")
+            #print(f"Search response for challenge '{challenge['name']}': {res2}")
+            for attribute in res2['results'][0]['attributes']:
+                if attribute['name'] == "Difficulty":
+                    ea.patch_attribute(attributeId=attribute['attributeId'], value=challenge['difficulty'])
+                if attribute['name'] == "Released":
+                    release_str = challenge['release_date']
+                    release_date = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ")
+                    formatted_release_date = release_date.strftime("%d %B %Y")
+                    ea.patch_attribute(attributeId=attribute['attributeId'], value=formatted_release_date)
+                if attribute['name'] == "Solved":
+                    if challenge['is_owned']:
+                        ea.patch_attribute(attributeId=attribute['attributeId'], value="done")
+                    else:
+                        ea.patch_attribute(attributeId=attribute['attributeId'], value=" ")
+                if attribute['name'] == "cssClass":
+                    if challenge['is_owned']:
+                        ea.patch_attribute(attributeId=attribute['attributeId'], value="done")
+                    else:
+                        ea.patch_attribute(attributeId=attribute['attributeId'], value="todo")
+
+        else: # doesnt already exist, create page
+            release_str = challenge['release_date']
+            release_date = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ")
+            formatted_release_date = release_date.strftime("%d %B %Y")
+            new_note = ea.create_note(
+                parentNoteId=catId,
+                type="text",
+                title=challenge['name'],
+                content=" ",
+            )
+            ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="relation", name="template", value=trilium_challenges_template_id)
+            ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Difficulty", value=challenge['difficulty'])
+            ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Released", value=formatted_release_date)
+            if challenge['is_owned']:
+                ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="done")
+                ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Solved", value="done")
+            else:
+                ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="todo")
+                ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Solved", value=" ")
+
+            print(f"[+] created ID: {challenge['id']}, Name: {challenge['name']}, Difficulty: {challenge['difficulty']}")
+
+print("[+] updating folder name                                          ")
+ea.patch_note(noteId=trilium_challenges_folder,title="Challenges - "+str(total_completed)+" / "+str(len(challenges)))
diff --git a/htb2trilium/htb2trilium_machines.py b/htb2trilium/htb2trilium_machines.py
new file mode 100644
index 0000000..de8be53
--- /dev/null
+++ b/htb2trilium/htb2trilium_machines.py
@@ -0,0 +1,203 @@
+import os
+import json
+import requests
+from bs4 import BeautifulSoup
+from datetime import datetime, timezone
+from htb_client import HTBClient
+from trilium_py.client import ETAPI
+
+# Get the absolute path of the script's directory
+script_dir = os.path.dirname(os.path.abspath(__file__))
+
+# Construct the full path to the config.json file
+config_path = os.path.join(script_dir, 'config.json')
+
+# Load configuration from the JSON file
+with open(config_path, 'r') as f:
+    config = json.load(f)
+
+# Accessing config values
+htb_code = config['htb_code']
+trilium_server_url = config['trilium_server_url']
+trilium_token = config['trilium_token']
+trilium_machines_htb_folder = config['trilium_machines_htb_folder']
+trilium_machines_template_id = config['trilium_machines_template_id']
+
+def generate_newpage(machine):
+	no = "<span style=\"color:hsl(0,75%,60%);\">No</span>"
+	yes = "<span style=\"color:hsl(120,75%,60%);\">Yes</span>"
+	user_colour = no
+	if machine['authUserInUserOwns']:
+		user_colour = yes
+	root_colour = no
+	if machine['authUserInRootOwns']:
+		root_colour = yes
+
+	status = "Retired"
+	if machine['active']:
+		status = "Active"
+
+	release_str = machine['release']
+	release_date = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ")
+	formatted_release_date = release_date.strftime("%d %B %Y")
+
+	html = """
+	<figure class="image image-style-align-left image_resized" style="width:12.28%;">
+	  <img src="https://www.hackthebox.com{avatar}">
+	</figure>
+	<p>
+	  <strong>OS:</strong> {os}<br>
+	  <strong>Difficulty:</strong> {difficultyText} <br>
+	  <strong>Rating:</strong> {rating} / 5<br>
+	  <strong>Points:</strong> {points}<br>
+	  <strong>User / Root: </strong> {user_colour} / {root_colour}<br>
+	  <strong>Released:</strong> {release_date}<br>
+	  <strong>State:</strong> {status}
+	</p>
+	<hr>
+	<h2>Notes</h2>
+	<p>&nbsp;</p>
+	""".format(
+		os=machine['os'],
+		difficultyText=machine['difficultyText'],
+		rating=machine['star'],
+		points=machine['points'],
+		release_date=formatted_release_date,
+		user_colour=user_colour,
+		root_colour=root_colour,
+		status = status,
+		avatar = machine['avatar'],
+	)
+
+	return html
+
+def get_timestamp(machine):
+    # Parse the release date string into a datetime object
+    release_str = machine['release']
+    dt = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ")
+    
+    # Set the timezone to UTC
+    dt = dt.replace(tzinfo=timezone.utc)
+    
+    # Get the timestamp
+    timestamp = dt.timestamp()
+    return timestamp
+
+print("[+] connecting to HTB")
+client = HTBClient(password=htb_code)
+print("[+] connecting to trilium")
+ea = ETAPI(trilium_server_url, trilium_token)
+print("[i] version: ", ea.app_info()['appVersion'])
+
+print("[i] HTB User:", client.user['id'], "-", client.user['name'])
+print("[i] user owns:", client.user['user_owns'], "| Root owns:", client.user['root_owns'], "| Respect:", client.user['respects'])
+
+master_folder = ea.get_note(trilium_machines_htb_folder)
+for attribute in master_folder['attributes']:
+	if attribute['name'] == "user":
+		if attribute['value'] != str(client.user['user_owns']):
+			print("[+] updating user owns (folder attribute)")
+			ea.patch_attribute(attributeId=attribute['attributeId'], value=str(client.user['user_owns']))
+	if attribute['name'] == "root":
+		if attribute['value'] != str(client.user['root_owns']):
+			print("[+] updating root owns (folder attribute)")
+			ea.patch_attribute(attributeId=attribute['attributeId'], value=str(client.user['root_owns']))
+	if attribute['name'] == "respect":
+		if attribute['value'] != str(client.user['respects']):
+			print("[+] updating respect (folder attribute)")
+			ea.patch_attribute(attributeId=attribute['attributeId'], value=str(client.user['respects']))
+
+print("[+] gathering machines info")
+machines = client.get_all_machines()
+machines.sort(key=get_timestamp)
+
+print(f"[i] Retrieved {len(machines)} machines")
+#for machine in machines:
+#    print(f" - ID: {machine['id']}, Name: {machine['name']}, OS: {machine['os']}, Difficulty: {machine['difficultyText']}")
+
+machine_count = 0
+completed_count = 0
+for machine in machines:
+	machine_count += 1
+	print('processing: ',machine_count, "/", len(machines), "("+machine['name']+")               " , end='\r')
+
+	res = ea.search_note(
+		search="\""+machine['name']+"\"",
+		ancestorNoteId=trilium_machines_htb_folder,
+		ancestorDepth='eq1',
+		limit=1,
+		fastSearch=True,
+	)
+
+	if res['results'] and machine['name'] == res['results'][0]['title']:
+		# page exists - lets check if the details have changed
+
+		current_html = ea.get_note_content(noteId=res['results'][0]['noteId'])
+		current_soup = BeautifulSoup(current_html, 'html.parser')
+		current_paragraph = current_soup.find_all('p')[0].text
+
+		new_html = generate_newpage(machine)
+		new_soup = BeautifulSoup(new_html, 'html.parser')
+		new_paragraph = new_soup.find_all('p')[0].text
+
+		# current page contains first paragraph of "blank" (useful for when it doesnt create or find the note properly.. shouldnt get here)
+		if current_paragraph == "blank":
+			ea.update_note_content(noteId=res['results'][0]['noteId'], content=new_html)
+			ea.create_attribute(attributeId=None, isInheritable=False, noteId=res['results'][0]['noteId'], type="relation", name="template", value=trilium_machines_htb_folder)
+			if machine['authUserInUserOwns'] and machine['authUserInRootOwns']:
+				ea.create_attribute(attributeId=None, isInheritable=False, noteId=res['results'][0]['noteId'], type="label", name="cssClass", value="done")
+			else:
+				if machine['authUserInUserOwns'] or machine['authUserInRootOwns']:
+					ea.create_attribute(attributeId=None, isInheritable=False, noteId=res['results'][0]['noteId'], type="label", name="cssClass", value="inprogress")
+				else:
+					ea.create_attribute(attributeId=None, isInheritable=False, noteId=res['results'][0]['noteId'], type="label", name="cssClass", value="todo")
+			# re-get the current content
+			current_html = ea.get_note_content(noteId=res['results'][0]['noteId'])
+			current_soup = BeautifulSoup(current_html, 'html.parser')
+
+		if current_paragraph != new_paragraph:
+
+			# details have updated!
+			print("[+] updating page:",machine['name'], "-> "+res['results'][0]['title']+"                 ")
+			replacement = current_soup.find('p')
+			replacement.replace_with( new_soup.find_all('p')[0] )
+			ea.update_note_content(noteId=res['results'][0]['noteId'], content=current_soup)
+
+			# now to update the label
+			for attribute in res['results'][0]['attributes']:
+				if attribute['name'] == "cssClass":
+					if machine['authUserInUserOwns'] and machine['authUserInRootOwns']:
+						ea.patch_attribute(attributeId=attribute['attributeId'], value="done")
+					else:
+						if machine['authUserInUserOwns'] or machine['authUserInRootOwns']:
+							ea.patch_attribute(attributeId=attribute['attributeId'], value="inprogress")
+						else:
+							ea.patch_attribute(attributeId=attribute['attributeId'], value="todo")
+							
+	else:
+		# title does not exist - create the note
+		html = generate_newpage(machine)
+		new_note = ea.create_note(
+			parentNoteId=trilium_machines_htb_folder,
+			type="text",
+			title=machine['name'],
+			content=html,
+		)
+		print("[+] created note:", machine['name'], "                        ")
+
+		ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="relation", name="template", value=trilium_machines_htb_folder)
+		if machine['authUserInUserOwns'] and machine['authUserInRootOwns']:
+			ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="done")
+		else:
+			if machine['authUserInUserOwns'] or machine['authUserInRootOwns']:
+				ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="inprogress")
+			else:
+				ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="todo")
+
+	if machine['authUserInUserOwns'] and machine['authUserInRootOwns']:
+			completed_count += 1
+
+print("[+] updating folder name                                          ")
+ea.patch_note(noteId=trilium_machines_htb_folder,title="Machines - "+str(completed_count)+" / "+str(len(machines)))
+
+print("[=] processed", machine_count)
\ No newline at end of file
diff --git a/htb2trilium/htb2trilium_sherlocks.py b/htb2trilium/htb2trilium_sherlocks.py
new file mode 100644
index 0000000..1e32e59
--- /dev/null
+++ b/htb2trilium/htb2trilium_sherlocks.py
@@ -0,0 +1,151 @@
+import os
+import json
+import requests
+from collections import defaultdict
+from bs4 import BeautifulSoup
+from datetime import datetime, timezone
+from htb_client import HTBClient
+from trilium_py.client import ETAPI
+
+# Get the absolute path of the script's directory
+script_dir = os.path.dirname(os.path.abspath(__file__))
+
+# Construct the full path to the config.json file
+config_path = os.path.join(script_dir, 'config.json')
+
+# Load configuration from the JSON file
+with open(config_path, 'r') as f:
+    config = json.load(f)
+
+# Accessing config values
+htb_code = config['htb_code']
+trilium_server_url = config['trilium_server_url']
+trilium_token = config['trilium_token']
+trilium_sherlocks_folder = config['trilium_sherlocks_folder']
+trilium_sherlocks_template_id = config['trilium_sherlocks_template_id']
+
+def get_timestamp(machine):
+    # Parse the release date string into a datetime object
+    release_str = machine['release_date']
+    dt = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ")
+    
+    # Set the timezone to UTC
+    dt = dt.replace(tzinfo=timezone.utc)
+    
+    # Get the timestamp
+    timestamp = dt.timestamp()
+    return timestamp
+
+print("[+] connecting to HTB")
+client = HTBClient(password=htb_code)
+print("[+] connecting to trilium")
+ea = ETAPI(trilium_server_url, trilium_token)
+print("[i] version: ", ea.app_info()['appVersion'])
+
+print("[i] HTB User:", client.user['id'], "-", client.user['name'])
+
+
+print("[+] gathering sherlocks info")
+categories = defaultdict(list)
+sherlocks = client.get_all_sherlocks()
+sherlocks.sort(key=get_timestamp)
+total_completed = 0  # Variable to track total completed sherlocks
+
+print(f"[i] Retrieved {len(sherlocks)} sherlocks")
+
+# Group sherlocks by their categories
+for sherlock in sherlocks:
+    categories[sherlock['category_name']].append(sherlock)
+    if sherlock['progress'] == 100:
+        total_completed += 1  # Increment total completed if challenge is owned
+
+# Print out the grouped sherlocks with the number of completed and total sherlocks in each category
+for category, grouped_sherlocks in categories.items():
+    total = len(grouped_sherlocks)
+    completed = sum(1 for sherlock in grouped_sherlocks if sherlock['progress'] == 100)  # Count completed challenges
+    
+    res = ea.search_note(
+        search=f"note.title %= '{category}*'",
+        ancestorNoteId=trilium_sherlocks_folder,
+        ancestorDepth='eq1',
+        limit=1,
+        fastSearch=True,
+    )
+    
+    catId = ""
+    if res['results'] and res['results'][0]['title'].split(' - ')[0].strip().lower() == category.lower():
+	# page exists - lets check if the details have changed
+        ea.patch_note(noteId=res['results'][0]['noteId'], title=category+" - "+str(completed)+" / "+str(total))
+        catId = res['results'][0]['noteId']
+        print(f"[i] updated category: {category} - ({completed}/{total})")
+    else:
+        new_note = ea.create_note(
+            parentNoteId=trilium_sherlocks_folder,
+            type="text",
+            title=category+" - "+str(completed)+" / "+str(total),
+            content=" ",
+        )
+        catId = new_note['note']['noteId']
+        print(f"[+] created category: {catId} {category} - ({completed}/{total})")
+    
+    for sherlock in grouped_sherlocks:
+        #print(f" - ID: {challenge['id']}, Name: {challenge['name']}, Difficulty: {challenge['difficulty']}")
+        res2 = ea.search_note(
+            search=f"{sherlock['name']}",
+            ancestorNoteId=catId,
+            ancestorDepth='eq1',
+            limit=1,
+            fastSearch=True,
+        )
+        # already exists update the values
+        if res2['results'] and res2['results'][0]['title'].lower() == sherlock['name'].lower():
+            print(f"[i] found ID: {sherlock['id']}, Name: {sherlock['name']}, Difficulty: {sherlock['difficulty']}")
+    
+            for attribute in res2['results'][0]['attributes']:
+                if attribute['name'] == "Difficulty":
+                    ea.patch_attribute(attributeId=attribute['attributeId'], value=sherlock['difficulty'])
+        
+                if attribute['name'] == "Released":
+                    release_str = sherlock['release_date']
+                    release_date = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ")
+                    formatted_release_date = release_date.strftime("%d %B %Y")
+                    ea.patch_attribute(attributeId=attribute['attributeId'], value=formatted_release_date)
+        
+                if attribute['name'] == "Solved":
+                    ea.patch_attribute(attributeId=attribute['attributeId'], value=str(sherlock['progress']))
+        
+                if attribute['name'] == "cssClass":
+                    # Determine the cssClass value based on the progress
+                    if sherlock['progress'] == 0:
+                        ea.patch_attribute(attributeId=attribute['attributeId'], value="todo")
+                    elif sherlock['progress'] == 100:
+                        ea.patch_attribute(attributeId=attribute['attributeId'], value="done")
+                    elif 0 < sherlock['progress'] < 100:
+                        ea.patch_attribute(attributeId=attribute['attributeId'], value="inprogress")
+
+
+        else: # doesnt already exist, create page
+            release_str = sherlock['release_date']
+            release_date = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ")
+            formatted_release_date = release_date.strftime("%d %B %Y")
+            new_note = ea.create_note(
+                parentNoteId=catId,
+                type="text",
+                title=sherlock['name'],
+                content=" ",
+            )
+            ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="relation", name="template", value=trilium_sherlocks_template_id)
+            ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Difficulty", value=sherlock['difficulty'])
+            ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Released", value=formatted_release_date)
+            ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Solved", value=str(sherlock['progress']))
+            if sherlock['progress'] == 0:
+                ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="todo")
+            elif sherlock['progress'] == 100:
+                ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="done")
+            elif 0 < sherlock['progress'] < 100:
+                ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="inprogress")
+
+            print(f"[+] created ID: {sherlock['id']}, Name: {sherlock['name']}, Difficulty: {sherlock['difficulty']}")
+
+print("[+] updating folder name                                          ")
+ea.patch_note(noteId=trilium_sherlocks_folder,title="Sherlocks - "+str(total_completed)+" / "+str(len(sherlocks)))
diff --git a/htb2trilium/htb_client.py b/htb2trilium/htb_client.py
new file mode 100644
index 0000000..a35929a
--- /dev/null
+++ b/htb2trilium/htb_client.py
@@ -0,0 +1,214 @@
+import requests
+import warnings
+
+warnings.filterwarnings("ignore")
+
+class HTBClient:
+    def __init__(self, password):
+        self.password = password
+        self.base_url = 'https://labs.hackthebox.com/api/v4'
+        self.proxies = {
+            #"http": "http://127.0.0.1:8080",  # Burp proxy for HTTP traffic
+            #"https": "http://127.0.0.1:8080"  # Burp proxy for HTTPS traffic
+        }
+        # Fetch user info
+        self.user = self.get_user_info()
+        
+        # Fetch user stats and store them in self.user
+        user_owns, root_owns, respects = self.get_user_stats(self.user['id'])
+        self.user['user_owns'] = user_owns
+        self.user['root_owns'] = root_owns
+        self.user['respects'] = respects
+
+    def get_user_info(self):
+        headers = {
+            "Authorization": f"Bearer {self.password}",
+            "User-Agent": None  # Explicitly remove User-Agent
+        }
+        response = requests.get(
+            f'{self.base_url}/user/info', 
+            headers=headers, 
+            proxies=self.proxies,
+            verify=False  # Disable SSL verification
+        )
+        if response.status_code != 200:
+            raise Exception(f"Error fetching {self.base_url}/user/info user info: {response.status_code}, {response.text}")
+        
+        # Return the user info as a dictionary
+        data = response.json().get('info')
+        return {'id': data['id'], 'name': data['name'], 'email': data['email']}
+
+    def get_user_stats(self, user_id):
+        headers = {
+            "Authorization": f"Bearer {self.password}",
+            "User-Agent": None  # Explicitly remove User-Agent
+        }
+        response = requests.get(
+            f'{self.base_url}/user/profile/basic/{user_id}', 
+            headers=headers, 
+            proxies=self.proxies,
+            verify=False  # Disable SSL verification
+        )
+        if response.status_code != 200:
+            raise Exception(f"Error fetching user stats: {response.status_code}, {response.text}")
+        
+        # Extract user statistics from the response
+        data = response.json().get('profile')
+        user_owns = data['user_owns']
+        root_owns = data['system_owns']
+        respects = data['respects']
+        return user_owns, root_owns, respects
+
+    def get_active_machines(self):
+        machines = []
+        seen_ids = set()  # Track unique machine IDs
+        seen_names = set()  # Track unique machine names
+        page = 1
+
+        while True:
+            response = requests.get(
+                f'{self.base_url}/machine/paginated?per_page=100&page={page}',
+                headers={
+                    "Authorization": f"Bearer {self.password}",
+                    "User-Agent": None  # Explicitly remove User-Agent
+                },
+                proxies=self.proxies,
+                verify=False  # Disable SSL verification
+            )
+
+            if response.status_code != 200:
+                raise Exception(f"Error fetching active machines: {response.status_code}, {response.text}")
+
+            data = response.json()
+            for machine in data['data']:
+                if machine['id'] not in seen_ids and machine['name'] not in seen_names:
+                    machines.append(machine)
+                    seen_ids.add(machine['id'])
+                    seen_names.add(machine['name'])
+
+            # Check for pagination
+            if page >= data['meta']['last_page']:
+                break
+            page += 1
+        
+        return machines
+
+    def get_retired_machines(self):
+        machines = []
+        seen_ids = set()  # Track unique machine IDs
+        seen_names = set()  # Track unique machine names
+        page = 1
+
+        while True:
+            response = requests.get(
+                f'{self.base_url}/machine/list/retired/paginated?per_page=100&page={page}',
+                headers={
+                    "Authorization": f"Bearer {self.password}",
+                    "User-Agent": None  # Explicitly remove User-Agent
+                },
+                proxies=self.proxies,
+                verify=False  # Disable SSL verification
+            )
+
+            if response.status_code != 200:
+                raise Exception(f"Error fetching retired machines: {response.status_code}, {response.text}")
+
+            data = response.json()
+            for machine in data['data']:
+                if machine['id'] not in seen_ids and machine['name'] not in seen_names:
+                    machines.append(machine)
+                    seen_ids.add(machine['id'])
+                    seen_names.add(machine['name'])
+
+            # Check for pagination
+            if page >= data['meta']['last_page']:
+                break
+            page += 1
+        
+        return machines
+
+    def get_all_machines(self):
+        # Combine active and retired machines, ensuring no duplicates
+        active_machines = self.get_active_machines()
+        retired_machines = self.get_retired_machines()
+
+        all_machines = active_machines + retired_machines
+        seen_ids = set()  # Track unique machine IDs
+        seen_names = set()  # Track unique machine names
+        unique_machines = []
+
+        for machine in all_machines:
+            if machine['id'] not in seen_ids and machine['name'] not in seen_names:
+                unique_machines.append(machine)
+                seen_ids.add(machine['id'])
+                seen_names.add(machine['name'])
+
+        return unique_machines
+
+    def get_all_challenges(self):
+        challenges = []
+        seen_ids = set()  # Track unique machine IDs
+        seen_names = set()  # Track unique machine names
+        page = 1
+
+        while True:
+            response = requests.get(
+                f'{self.base_url}/challenges?per_page=100&page={page}',
+                headers={
+                    "Authorization": f"Bearer {self.password}",
+                    "User-Agent": None  # Explicitly remove User-Agent
+                },
+                proxies=self.proxies,
+                verify=False  # Disable SSL verification
+            )
+
+            if response.status_code != 200:
+                raise Exception(f"Error fetching challenges: {response.status_code}, {response.text}")
+
+            data = response.json()
+            for challenge in data['data']:
+                if challenge['id'] not in seen_ids and challenge['name'] not in seen_names:
+                    challenges.append(challenge)
+                    seen_ids.add(challenge['id'])
+                    seen_names.add(challenge['name'])
+
+            # Check for pagination
+            if page >= data['meta']['last_page']:
+                break
+            page += 1
+        
+        return challenges
+
+    def get_all_sherlocks(self):
+        sherlocks = []
+        seen_ids = set()  # Track unique machine IDs
+        seen_names = set()  # Track unique machine names
+        page = 1
+
+        while True:
+            response = requests.get(
+                f'{self.base_url}/sherlocks?per_page=100&page={page}',
+                headers={
+                    "Authorization": f"Bearer {self.password}",
+                    "User-Agent": None  # Explicitly remove User-Agent
+                },
+                proxies=self.proxies,
+                verify=False  # Disable SSL verification
+            )
+
+            if response.status_code != 200:
+                raise Exception(f"Error fetching sherlocks: {response.status_code}, {response.text}")
+
+            data = response.json()
+            for sherlock in data['data']:
+                if sherlock['id'] not in seen_ids and sherlock['name'] not in seen_names:
+                    sherlocks.append(sherlock)
+                    seen_ids.add(sherlock['id'])
+                    seen_names.add(sherlock['name'])
+
+            # Check for pagination
+            if page >= data['meta']['last_page']:
+                break
+            page += 1
+        
+        return sherlocks
\ No newline at end of file

diff --git a/README.md b/README.md
index f623b41..fefccb6 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,58 @@
 TriliumScripts
 ===============
 
-My collection of scripts for use with trilium
\ No newline at end of file
+Collection of scripts for use with trilium
+
+## syntax highlighting
+
+**highlight_moodlog.js** & **highlight_todo.js** - Both of these will apply appropriate syntaxy highlighting to the specific note.
+
+### installation
+
+1) add the code as the notes contents.
+
+2) set "note type" to "JS frontend"
+
+3) add an "owned attribute" of "#widget"
+
+### use
+
+on the note you want highlighting set the "owned attributes" to "#todotxt #hideHighlightWidget" or "#moodlog #hideHighlightWidget" depending on which syntax type you want to highlight
+
+## bidirectional sync
+
+**trilium_sync.php** - script to keep a folder of local text files and a trilium note in sync.
+
+### installation
+
+Edit the config values at the top of the script to match your trilium setup. Run it manually to test it is working as expected. If it is create a cron job.
+
+### example use
+
+```
+$> php trilium_sync.php 
+[i] checking connections
+[i] version: x.xx.x
+[i] gathering info
+
+- Folders -
+Folder/Path              Folder Name              Parent       noteId       Mod Trl    Mod Dsk    Status         
+Synced/                  Synced                   root         j0DRZhIzV97W 1742502936 1742507367 Exists
+Synced/BothPlaces/       BothPlaces               j0DRZhIzV97W AQb3Dg0yZHSu 1742507075 1742507200 Exists
+Synced/[del] tobe del/   [del] tobe del           j0DRZhIzV97W j0xUs1FdZL1Y 1742507321            Missing_Disk   
+Synced/AddToTrilium/     AddToTrilium                                                  1742507383 Missing_Trilium
+Synced/tobe deleted/     tobe deleted                                                  1742507200 Missing_Trilium
+- Files -
+Folder/Path              Filename                 Parent       noteId       Mod Trl    Mod Dsk    Hash  Status         
+Synced/BothPlaces/       UpdateDiskFile           AQb3Dg0yZHSu IwUZLwWdbiWG 1742507314 1742507199 Diff  Update_Disk    
+Synced/BothPlaces/       UpdateTriliumFile        AQb3Dg0yZHSu 7pMPgsb9AVRf 1742507130 1742507345 Diff  Update_Trilium 
+Synced/[del] tobe del/   gone_also.txt            j0xUs1FdZL1Y 4wOoHDy07csr 1742507178            N/A   Missing_Disk   
+Synced/AddToTrilium/     fromDiskToTrilium.txt                                         1742507394 N/A   Missing_Trilium
+Synced/tobe deleted/     gone_also.txt                                                 1742507200 N/A   Missing_Trilium
+
+[-] deleting disk folder: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/tobe deleted/
+[+] creating trilium folder: Synced/AddToTrilium/
+[+] creating trilium note: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/AddToTrilium/fromDiskToTrilium.txt
+[+] updating file on disk: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/BothPlaces/UpdateDiskFile
+[+] updating file on trilium: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/BothPlaces/UpdateTriliumFile
+```
\ No newline at end of file
diff --git a/highlight_moodlog.js b/highlight_moodlog.js
new file mode 100644
index 0000000..be4c283
--- /dev/null
+++ b/highlight_moodlog.js
@@ -0,0 +1,165 @@
+const TPL = `<div style="padding: 10px; border-top: 1px solid var(--main-border-color); contain: none;">
+    <strong>Moodlog Highlighting Applied</strong>
+</div>`;
+
+class MoodlogHighlighterWidget extends api.NoteContextAwareWidget {
+    static get parentWidget() { 
+        //console.log("MDLG: Getting parent widget: center-pane");
+        return 'center-pane'; 
+    }
+
+    get position() { 
+        //console.log("MDLG: Setting position: 100");
+        return 100; 
+    }
+
+    isEnabled() {
+        const enabled = super.isEnabled() 
+            && this.note.type === 'text' 
+            && this.note.hasLabel('moodlog');
+        //console.log(`MDLG: isEnabled: ${enabled}`);
+        return enabled;
+    }
+
+    doRender() {
+        //console.log("MDLG: Rendering widget...");
+        this.$widget = $(TPL);
+        //console.log("MDLG: Widget rendered:", this.$widget);
+        return this.$widget;
+    }
+
+    async refreshWithNote(note) {
+        //console.log("MDLG: Refreshing with note:", note);
+        const { content } = await note.getNoteComplement();
+        //console.log("MDLG: Note content fetched:", content);
+
+        // Check if content is fetched properly
+        if (!content) {
+            //console.log("MDLG: No content fetched. Exiting refresh.");
+            return;
+        }
+
+        const highlightedContent = this.applyHighlighting(content);
+        //console.log("MDLG: Highlighted content:", highlightedContent);
+
+        if (highlightedContent !== content) {
+            //console.log("MDLG: Updating note content with highlighted version");
+
+            // Use the setNoteContent function to update the note content
+            try {
+                await setNoteContent(note.noteId, highlightedContent);
+                //console.log("MDLG: Content updated with highlighted version");
+            } catch (error) {
+                //console.error("MDLG: Error updating content:", error);
+            }
+        } else {
+            //console.log("MDLG: No changes to content, skipping update.");
+        }
+    }
+
+
+ applyHighlighting(content) {
+    //console.log("MDLG: Applying highlighting to content...");
+
+    // Replace <br> with \n, then remove all other HTML tags
+    const plainTextContent = content.replace(/<br\s*\/?>/gi, '\n').replace(/<[^>]*>/g, ' ');
+    //console.log("MDLG: Stripped content:", plainTextContent);
+
+    // Combined regex for date & time
+    const dateTimePattern = /([1-2]\d{3}[-/.](0?[1-9]|1[0-2])[-/.](0[1-9]|[12]\d|3[01]))\s+(\d{1,2}:\d{2})/g;
+    const projectPattern = /\[\S+?\]/g;
+    const contextPattern = /(\s|^)(@\S+)/g;
+    const hashtagPattern = /(\s|^)(#\S+)/g;
+    const positivePattern = /(\s|^)(\+\S+)/g;
+    const negativePattern = /(\s|^)(-\S+)/g;
+
+    // Escape HTML characters to prevent issues with replacements
+    function escapeHtml(str) {
+        return str.replace(/[&<>"']/g, function (char) {
+            return {
+                '&': '&amp;',
+                '<': '&lt;',
+                '>': '&gt;',
+                '"': '&quot;',
+                "'": '&#039;'
+            }[char];
+        });
+    }
+
+      // Helper function to map the number to a colour based on the gradient
+    function getProjectColour(number) {
+        const minColor = { r: 107, g: 109, b: 229 };  // #6b6de5
+        const maxColor = { r: 70, g: 226, b: 96 };   // #46e260
+
+        const ratio = number / 9; // since we are working with 0-9
+
+        // Interpolate the RGB values between minColor and maxColor
+        const r = Math.round(minColor.r + ratio * (maxColor.r - minColor.r));
+        const g = Math.round(minColor.g + ratio * (maxColor.g - minColor.g));
+        const b = Math.round(minColor.b + ratio * (maxColor.b - minColor.b));
+
+        return `rgb(${r}, ${g}, ${b})`;
+    }
+     
+    // Process each line of the plain text content
+    let highlightedLines = plainTextContent.split("\n").map(line => {
+        //console.log("MDLG: Processing line:", line);
+
+        // Apply combined date & time highlight
+        line = line.replace(dateTimePattern, `<span style="color: rgb(188, 55, 237);">$1</span> <span style="color: rgb(188, 55, 237);">$4</span>`);
+
+        // Apply other highlights
+        line = line.replace(projectPattern, (match) => {
+            // Extract the number inside square brackets
+            const number = parseInt(match.match(/\d+/)[0], 10);
+            const colour = getProjectColour(number);
+            return `<span style="color: ${colour}; font-weight: bold;">${escapeHtml(match)}</span>`;
+        });
+        
+        // Apply other highlights
+        //line = line.replace(projectPattern, `<span style="color: rgb(224, 218, 36); font-weight: bold;">$&</span>`);
+        line = line.replace(contextPattern, (_, space, tag) => `${space}<span style="color: rgb(255 165 10);">${escapeHtml(tag)}</span>`);
+        line = line.replace(hashtagPattern, (_, space, tag) => `${space}<span style="color: rgb(41, 229, 242);">${escapeHtml(tag)}</span>`);
+        line = line.replace(positivePattern, (_, space, tag) => `${space}<span style="color: rgb(44, 227, 34);">${escapeHtml(tag)}</span>`);
+        line = line.replace(negativePattern, (_, space, tag) => `${space}<span style="color: rgb(230, 59, 44);">${escapeHtml(tag)}</span>`);
+
+        return line.trim();
+    });
+
+    // Join lines with proper <br> handling
+    let finalContent = highlightedLines.join("<br>");
+
+    //console.log("MDLG: Highlighted content:", finalContent);
+    return finalContent;
+}
+
+
+
+
+    async entitiesReloadedEvent({ loadResults }) {
+        //console.log("MDLG: Entities reloaded event triggered");
+        if (loadResults.isNoteContentReloaded(this.noteId)) {
+            //console.log("MDLG: Note content reloaded, refreshing widget...");
+            //this.refresh();
+        } else {
+            //console.log("MDLG: No content reload detected.");
+        }
+    }
+    
+
+}
+
+console.log("MDLG: Starting Moodlog Highlighter Widget");
+module.exports = MoodlogHighlighterWidget;
+
+function setNoteContent(noteId, content) {
+    /*const noteElement = document.querySelector(`note-detail-readonly-text-content`);
+
+    if (noteElement) {
+        noteElement.innerHTML = content; // Update the displayed content
+        console.log(`MDLG: Updated note ${noteId} in the DOM.`);
+    } else {
+        console.warn(`MDLG: Note element with data-note-id='${noteId}' not found.`);
+    }*/
+    return api.runOnBackend((id, data) => api.getNote(id).setContent(data), [noteId, content]);
+}
diff --git a/highlight_todotxt.js b/highlight_todotxt.js
new file mode 100644
index 0000000..1f1b007
--- /dev/null
+++ b/highlight_todotxt.js
@@ -0,0 +1,157 @@
+const TPL = `<div style="padding: 10px; border-top: 1px solid var(--main-border-color); contain: none;">
+    <strong>Todo Highlighting Applied</strong>
+</div>`;
+
+class MoodlogHighlighterWidget extends api.NoteContextAwareWidget {
+    static get parentWidget() { 
+        //console.log("MDLG: Getting parent widget: center-pane");
+        return 'center-pane'; 
+    }
+
+    get position() { 
+        //console.log("MDLG: Setting position: 100");
+        return 100; 
+    }
+
+    isEnabled() {
+        const enabled = super.isEnabled() 
+            && this.note.type === 'text' 
+            && this.note.hasLabel('todotxt');
+        //console.log(`MDLG: isEnabled: ${enabled}`);
+        return enabled;
+    }
+
+    doRender() {
+        //console.log("MDLG: Rendering widget...");
+        this.$widget = $(TPL);
+        //console.log("MDLG: Widget rendered:", this.$widget);
+        return this.$widget;
+    }
+
+    async refreshWithNote(note) {
+        //console.log("MDLG: Refreshing with note:", note);
+        const { content } = await note.getNoteComplement();
+        //console.log("MDLG: Note content fetched:", content);
+
+        // Check if content is fetched properly
+        if (!content) {
+            //console.log("MDLG: No content fetched. Exiting refresh.");
+            return;
+        }
+
+        const highlightedContent = this.applyHighlighting(content);
+        //console.log("MDLG: Highlighted content:", highlightedContent);
+
+        if (highlightedContent !== content) {
+            //console.log("MDLG: Updating note content with highlighted version");
+
+            // Use the setNoteContent function to update the note content
+            try {
+                await setNoteContent(note.noteId, highlightedContent);
+                //console.log("MDLG: Content updated with highlighted version");
+            } catch (error) {
+                //console.error("MDLG: Error updating content:", error);
+            }
+        } else {
+            //console.log("MDLG: No changes to content, skipping update.");
+        }
+    }
+
+
+applyHighlighting(content) {
+    // Replace <br> with \n, then remove all other HTML tags
+    const plainTextContent = content.replace(/<br\s*\/?\>/gi, '\n').replace(/<[^>]*>/g, ' ');
+
+    // Regular expressions for Todo.txt syntax
+    const datePattern = /\b(0[1-9]|[12]\d|3[01])\/(0[1-9]|1[0-2])\/([1-2]\d{3})\b/g;
+    const completedTaskPattern = /^\s*x\s+.*/g;
+    const priorityPattern = /^(\s*\([A-E]\))\s+/;
+    const projectPattern = /(?:\s|^)(\+\S+)/g;
+    const contextPattern = /(?:\s|^)(@\S+)/g;
+    const waitPattern = /\bWAIT\b/gi;
+    const customAttributePattern = /(\s+[^\s:]+:[^\s:]+)+\s*$/g;
+    
+    function escapeHtml(str) {
+        return str.replace(/[&<>"']/g, char => ({
+            '&': '&amp;', '<': '&lt;', '>': '&gt;',
+            '"': '&quot;', "'": '&#039;'
+        })[char]);
+    }
+
+    let highlightedLines = plainTextContent.split("\n").map(line => {
+        // If line starts with 'x', make the whole line grey
+        if (/^\s*x\s+/.test(line)) {
+            return `<span style="color: grey;">${line}</span>`;
+        }
+        
+        // Highlight priority with different colours based on level
+        line = line.replace(priorityPattern, (match, priority) => {
+            const level = priority.match(/[A-E]/)?.[0];
+            const colors = {
+                'A': 'rgb(255, 0, 0)',       // Red
+                'B': 'rgb(255, 102, 0)',     // Orange
+                'C': 'rgb(255, 204, 0)',     // Yellow
+                'D': 'rgb(0, 153, 255)',     // Blue
+                'E': 'rgb(102, 204, 102)'    // Green
+            };
+            const color = colors[level] || 'black';
+            return `<span style="color: ${color}; font-weight: bold;">${escapeHtml(priority)}</span> `;
+        });
+        
+        // Highlight date
+        line = line.replace(datePattern, (match) => {
+            return `<span style="color: rgb(188, 55, 237);">${escapeHtml(match)}</span>`;
+        });
+
+        // Highlight project tags
+        line = line.replace(projectPattern, (_, tag) => `<span style="color: rgb(224, 218, 36);"> ${escapeHtml(tag)}</span>`);
+        
+        // Highlight context tags
+        line = line.replace(contextPattern, (_, tag) => `<span style="color: rgb(255, 165, 10);"> ${escapeHtml(tag)}</span>`);
+        
+        // Highlight WAIT command
+        line = line.replace(waitPattern, `<span style="color: rgb(255, 69, 0);">WAIT</span>`);
+        
+        // Highlight custom attributes
+        line = line.replace(customAttributePattern, `<span style="color: rgb(100, 149, 237);">$1</span>`);
+        
+        // Remove leading space (if any) from the beginning of the line, without affecting spaces before + or @
+        line = line.replace(/^\s+/, '');
+        
+        return line.trim();
+    });
+
+    return highlightedLines.join("<br>");
+}
+
+
+
+
+
+    async entitiesReloadedEvent({ loadResults }) {
+        //console.log("MDLG: Entities reloaded event triggered");
+        if (loadResults.isNoteContentReloaded(this.noteId)) {
+            //console.log("MDLG: Note content reloaded, refreshing widget...");
+            //this.refresh();
+        } else {
+            //console.log("MDLG: No content reload detected.");
+        }
+    }
+    
+
+}
+
+console.log("TodoHighlight: Starting todotxt Highlighter Widget");
+module.exports = MoodlogHighlighterWidget;
+
+function setNoteContent(noteId, content) {
+    /*const noteElement = document.querySelector(`note-detail-readonly-text-content`);
+
+    if (noteElement) {
+        noteElement.innerHTML = content; // Update the displayed content
+        console.log(`MDLG: Updated note ${noteId} in the DOM.`);
+    } else {
+        console.warn(`MDLG: Note element with data-note-id='${noteId}' not found.`);
+    }*/
+    return api.runOnBackend((id, data) => api.getNote(id).setContent(data), [noteId, content]);
+}
diff --git a/htb2trilium/INSTALL.md b/htb2trilium/INSTALL.md
new file mode 100644
index 0000000..da8afad
--- /dev/null
+++ b/htb2trilium/INSTALL.md
@@ -0,0 +1,41 @@
+## scripts require:
+
+```
+ > pip3 install trilium-py
+```
+
+## config.json
+- get trilium token in trilium options->ETAPI and "create new token"
+- create a new note with a title "Machines"
+- click on this note -> "note info". Note ID is there. This ID goes in "trilium_machines_htb_folder" in the config file
+- set the folder's owned attributes to:
+
+```
+#label:user=promoted,single,text #label:root=promoted,single,text #label:respect=promoted,single,text #user=0 #root=0 #respect=0 
+```
+
+ - create a note named "HTBMachineTemplate" 
+ - set it's "owned attributes" to:
+
+```
+#template #label:User=promoted,single,text #label:Root=promoted,single,text #label:Tags=promoted,single,text
+```
+
+- get this page's ID and put in "trilium_machines_template_id"
+- create "challenges" page, this page's ID goes in: trilium_challenges_folder  
+- create "Sherlocks" page, this page's ID goes in: trilium_sherlocks_folder  
+- create 2 pages "HTBChallengesTemplate" and "HTBSherlocksTemplate", both of these should have the owned attributes:
+
+```
+#template #label:Difficulty=promoted,single,text #label:Solved=promoted,single,text #label:Released=promoted,single,text 
+```
+
+- The ID's of "HTBChallengesTemplate" and "HTBSherlocksTemplate" go in the matching config values.
+
+- to get "in progress" todo colour - add to trilium demo/scripting/taskmanager/implementation/CSS:
+
+```
+ span.fancytree-node.inprogress .fancytree-title {
+    color: orange !important;
+ }
+```
\ No newline at end of file
diff --git a/htb2trilium/README.md b/htb2trilium/README.md
new file mode 100644
index 0000000..1b45ae2
--- /dev/null
+++ b/htb2trilium/README.md
@@ -0,0 +1,16 @@
+htb2trilium
+===============
+
+Pull hackthebox info and stats into personal trilium note taking software
+
+single machine view:
+
+![machine view](images/machine_view.png)
+
+overview of machines:
+
+![machines overview](images/machines_overview.png)
+
+navigation:
+
+![navigation](images/navigation.png)
\ No newline at end of file
diff --git a/htb2trilium/config.json b/htb2trilium/config.json
new file mode 100644
index 0000000..39597ce
--- /dev/null
+++ b/htb2trilium/config.json
@@ -0,0 +1,11 @@
+{
+	"htb_code": "in https://app.hackthebox.com/profile/settings click 'profile settings' and under 'app tokens' you can 'create app token' - that goes here",
+	"trilium_server_url": "https://place.to.your.trilium.server",
+	"trilium_token": "",
+	"trilium_machines_htb_folder": "",
+	"trilium_machines_template_id": "",
+	"trilium_challenges_folder": "",
+	"trilium_challenges_template_id": "",
+	"trilium_sherlocks_folder": "",
+	"trilium_sherlocks_template_id": ""
+}
\ No newline at end of file
diff --git a/htb2trilium/htb2trilium_challenges.py b/htb2trilium/htb2trilium_challenges.py
new file mode 100644
index 0000000..d0d95ca
--- /dev/null
+++ b/htb2trilium/htb2trilium_challenges.py
@@ -0,0 +1,146 @@
+import os
+import json
+import requests
+from collections import defaultdict
+from bs4 import BeautifulSoup
+from datetime import datetime, timezone
+from htb_client import HTBClient
+from trilium_py.client import ETAPI
+
+# Get the absolute path of the script's directory
+script_dir = os.path.dirname(os.path.abspath(__file__))
+
+# Construct the full path to the config.json file
+config_path = os.path.join(script_dir, 'config.json')
+
+# Load configuration from the JSON file
+with open(config_path, 'r') as f:
+    config = json.load(f)
+
+# Accessing config values
+htb_code = config['htb_code']
+trilium_server_url = config['trilium_server_url']
+trilium_token = config['trilium_token']
+trilium_challenges_folder = config['trilium_challenges_folder']
+trilium_challenges_template_id = config['trilium_challenges_template_id']
+
+def get_timestamp(machine):
+    # Parse the release date string into a datetime object
+    release_str = machine['release_date']
+    dt = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ")
+    
+    # Set the timezone to UTC
+    dt = dt.replace(tzinfo=timezone.utc)
+    
+    # Get the timestamp
+    timestamp = dt.timestamp()
+    return timestamp
+
+print("[+] connecting to HTB")
+client = HTBClient(password=htb_code)
+print("[+] connecting to trilium")
+ea = ETAPI(trilium_server_url, trilium_token)
+print("[i] version: ", ea.app_info()['appVersion'])
+
+print("[i] HTB User:", client.user['id'], "-", client.user['name'])
+
+
+print("[+] gathering challenges info")
+categories = defaultdict(list)
+challenges = client.get_all_challenges()
+challenges.sort(key=get_timestamp)
+total_completed = 0  # Variable to track total completed challenges
+
+print(f"[i] Retrieved {len(challenges)} challenges")
+
+# Group challenges by their categories
+for challenge in challenges:
+    categories[challenge['category_name']].append(challenge)
+    if challenge['is_owned']:
+        total_completed += 1  # Increment total completed if challenge is owned
+
+# Print out the grouped challenges with the number of completed and total challenges in each category
+for category, grouped_challenges in categories.items():
+    total = len(grouped_challenges)
+    completed = sum(1 for challenge in grouped_challenges if challenge['is_owned'])  # Count completed challenges
+    
+    res = ea.search_note(
+        search=f"note.title %= '{category}*'",
+        ancestorNoteId=trilium_challenges_folder,
+        ancestorDepth='eq1',
+        limit=1,
+        fastSearch=True,
+    )
+    
+    catId = ""
+    if res['results'] and res['results'][0]['title'].split(' - ')[0].strip().lower() == category.lower():
+        # page exists - lets check if the details have changed
+        ea.patch_note(noteId=res['results'][0]['noteId'], title=category+" - "+str(completed)+" / "+str(total))
+        catId = res['results'][0]['noteId']
+        print(f"[i] updated category: {category} - ({completed}/{total})")
+    else:
+        new_note = ea.create_note(
+            parentNoteId=trilium_challenges_folder,
+            type="text",
+            title=category+" - "+str(completed)+" / "+str(total),
+            content=" ",
+        )
+        catId = new_note['note']['noteId']
+        print(f"[+] created category: {catId} {category} - ({completed}/{total})")
+    
+    for challenge in grouped_challenges:
+        #print(f" - ID: {challenge['id']}, Name: {challenge['name']}, Difficulty: {challenge['difficulty']}")
+        res2 = ea.search_note(
+            search=f"{challenge['name']}",
+            ancestorNoteId=catId,
+            ancestorDepth='eq1',
+            limit=1,
+            fastSearch=True,
+        )
+        # already exists update the values
+        if res2['results'] and res2['results'][0]['title'].lower() == challenge['name'].lower():
+            print(f"[i] found ID: {challenge['id']}, Name: {challenge['name']}, Difficulty: {challenge['difficulty']}")
+            #print(f"Search response for challenge '{challenge['name']}': {res2}")
+            for attribute in res2['results'][0]['attributes']:
+                if attribute['name'] == "Difficulty":
+                    ea.patch_attribute(attributeId=attribute['attributeId'], value=challenge['difficulty'])
+                if attribute['name'] == "Released":
+                    release_str = challenge['release_date']
+                    release_date = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ")
+                    formatted_release_date = release_date.strftime("%d %B %Y")
+                    ea.patch_attribute(attributeId=attribute['attributeId'], value=formatted_release_date)
+                if attribute['name'] == "Solved":
+                    if challenge['is_owned']:
+                        ea.patch_attribute(attributeId=attribute['attributeId'], value="done")
+                    else:
+                        ea.patch_attribute(attributeId=attribute['attributeId'], value=" ")
+                if attribute['name'] == "cssClass":
+                    if challenge['is_owned']:
+                        ea.patch_attribute(attributeId=attribute['attributeId'], value="done")
+                    else:
+                        ea.patch_attribute(attributeId=attribute['attributeId'], value="todo")
+
+        else: # doesnt already exist, create page
+            release_str = challenge['release_date']
+            release_date = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ")
+            formatted_release_date = release_date.strftime("%d %B %Y")
+            new_note = ea.create_note(
+                parentNoteId=catId,
+                type="text",
+                title=challenge['name'],
+                content=" ",
+            )
+            ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="relation", name="template", value=trilium_challenges_template_id)
+            ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Difficulty", value=challenge['difficulty'])
+            ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Released", value=formatted_release_date)
+            if challenge['is_owned']:
+                ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="done")
+                ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Solved", value="done")
+            else:
+                ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="todo")
+                ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Solved", value=" ")
+
+            print(f"[+] created ID: {challenge['id']}, Name: {challenge['name']}, Difficulty: {challenge['difficulty']}")
+
+print("[+] updating folder name                                          ")
+ea.patch_note(noteId=trilium_challenges_folder,title="Challenges - "+str(total_completed)+" / "+str(len(challenges)))
diff --git a/htb2trilium/htb2trilium_machines.py b/htb2trilium/htb2trilium_machines.py
new file mode 100644
index 0000000..de8be53
--- /dev/null
+++ b/htb2trilium/htb2trilium_machines.py
@@ -0,0 +1,203 @@
+import os
+import json
+import requests
+from bs4 import BeautifulSoup
+from datetime import datetime, timezone
+from htb_client import HTBClient
+from trilium_py.client import ETAPI
+
+# Get the absolute path of the script's directory
+script_dir = os.path.dirname(os.path.abspath(__file__))
+
+# Construct the full path to the config.json file
+config_path = os.path.join(script_dir, 'config.json')
+
+# Load configuration from the JSON file
+with open(config_path, 'r') as f:
+    config = json.load(f)
+
+# Accessing config values
+htb_code = config['htb_code']
+trilium_server_url = config['trilium_server_url']
+trilium_token = config['trilium_token']
+trilium_machines_htb_folder = config['trilium_machines_htb_folder']
+trilium_machines_template_id = config['trilium_machines_template_id']
+
+def generate_newpage(machine):
+	no = "<span style=\"color:hsl(0,75%,60%);\">No</span>"
+	yes = "<span style=\"color:hsl(120,75%,60%);\">Yes</span>"
+	user_colour = no
+	if machine['authUserInUserOwns']:
+		user_colour = yes
+	root_colour = no
+	if machine['authUserInRootOwns']:
+		root_colour = yes
+
+	status = "Retired"
+	if machine['active']:
+		status = "Active"
+
+	release_str = machine['release']
+	release_date = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ")
+	formatted_release_date = release_date.strftime("%d %B %Y")
+
+	html = """
+	<figure class="image image-style-align-left image_resized" style="width:12.28%;">
+	  <img src="https://www.hackthebox.com{avatar}">
+	</figure>
+	<p>
+	  <strong>OS:</strong> {os}<br>
+	  <strong>Difficulty:</strong> {difficultyText} <br>
+	  <strong>Rating:</strong> {rating} / 5<br>
+	  <strong>Points:</strong> {points}<br>
+	  <strong>User / Root: </strong> {user_colour} / {root_colour}<br>
+	  <strong>Released:</strong> {release_date}<br>
+	  <strong>State:</strong> {status}
+	</p>
+	<hr>
+	<h2>Notes</h2>
+	<p>&nbsp;</p>
+	""".format(
+		os=machine['os'],
+		difficultyText=machine['difficultyText'],
+		rating=machine['star'],
+		points=machine['points'],
+		release_date=formatted_release_date,
+		user_colour=user_colour,
+		root_colour=root_colour,
+		status = status,
+		avatar = machine['avatar'],
+	)
+
+	return html
+
+def get_timestamp(machine):
+    # Parse the release date string into a datetime object
+    release_str = machine['release']
+    dt = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ")
+    
+    # Set the timezone to UTC
+    dt = dt.replace(tzinfo=timezone.utc)
+    
+    # Get the timestamp
+    timestamp = dt.timestamp()
+    return timestamp
+
+print("[+] connecting to HTB")
+client = HTBClient(password=htb_code)
+print("[+] connecting to trilium")
+ea = ETAPI(trilium_server_url, trilium_token)
+print("[i] version: ", ea.app_info()['appVersion'])
+
+print("[i] HTB User:", client.user['id'], "-", client.user['name'])
+print("[i] user owns:", client.user['user_owns'], "| Root owns:", client.user['root_owns'], "| Respect:", client.user['respects'])
+
+master_folder = ea.get_note(trilium_machines_htb_folder)
+for attribute in master_folder['attributes']:
+	if attribute['name'] == "user":
+		if attribute['value'] != str(client.user['user_owns']):
+			print("[+] updating user owns (folder attribute)")
+			ea.patch_attribute(attributeId=attribute['attributeId'], value=str(client.user['user_owns']))
+	if attribute['name'] == "root":
+		if attribute['value'] != str(client.user['root_owns']):
+			print("[+] updating root owns (folder attribute)")
+			ea.patch_attribute(attributeId=attribute['attributeId'], value=str(client.user['root_owns']))
+	if attribute['name'] == "respect":
+		if attribute['value'] != str(client.user['respects']):
+			print("[+] updating respect (folder attribute)")
+			ea.patch_attribute(attributeId=attribute['attributeId'], value=str(client.user['respects']))
+
+print("[+] gathering machines info")
+machines = client.get_all_machines()
+machines.sort(key=get_timestamp)
+
+print(f"[i] Retrieved {len(machines)} machines")
+#for machine in machines:
+#    print(f" - ID: {machine['id']}, Name: {machine['name']}, OS: {machine['os']}, Difficulty: {machine['difficultyText']}")
+
+machine_count = 0
+completed_count = 0
+for machine in machines:
+	machine_count += 1
+	print('processing: ',machine_count, "/", len(machines), "("+machine['name']+")               " , end='\r')
+
+	res = ea.search_note(
+		search="\""+machine['name']+"\"",
+		ancestorNoteId=trilium_machines_htb_folder,
+		ancestorDepth='eq1',
+		limit=1,
+		fastSearch=True,
+	)
+
+	if res['results'] and machine['name'] == res['results'][0]['title']:
+		# page exists - lets check if the details have changed
+
+		current_html = ea.get_note_content(noteId=res['results'][0]['noteId'])
+		current_soup = BeautifulSoup(current_html, 'html.parser')
+		current_paragraph = current_soup.find_all('p')[0].text
+
+		new_html = generate_newpage(machine)
+		new_soup = BeautifulSoup(new_html, 'html.parser')
+		new_paragraph = new_soup.find_all('p')[0].text
+
+		# current page contains first paragraph of "blank" (useful for when it doesnt create or find the note properly.. shouldnt get here)
+		if current_paragraph == "blank":
+			ea.update_note_content(noteId=res['results'][0]['noteId'], content=new_html)
+			ea.create_attribute(attributeId=None, isInheritable=False, noteId=res['results'][0]['noteId'], type="relation", name="template", value=trilium_machines_htb_folder)
+			if machine['authUserInUserOwns'] and machine['authUserInRootOwns']:
+				ea.create_attribute(attributeId=None, isInheritable=False, noteId=res['results'][0]['noteId'], type="label", name="cssClass", value="done")
+			else:
+				if machine['authUserInUserOwns'] or machine['authUserInRootOwns']:
+					ea.create_attribute(attributeId=None, isInheritable=False, noteId=res['results'][0]['noteId'], type="label", name="cssClass", value="inprogress")
+				else:
+					ea.create_attribute(attributeId=None, isInheritable=False, noteId=res['results'][0]['noteId'], type="label", name="cssClass", value="todo")
+			# re-get the current content
+			current_html = ea.get_note_content(noteId=res['results'][0]['noteId'])
+			current_soup = BeautifulSoup(current_html, 'html.parser')
+
+		if current_paragraph != new_paragraph:
+
+			# details have updated!
+			print("[+] updating page:",machine['name'], "-> "+res['results'][0]['title']+"                 ")
+			replacement = current_soup.find('p')
+			replacement.replace_with( new_soup.find_all('p')[0] )
+			ea.update_note_content(noteId=res['results'][0]['noteId'], content=current_soup)
+
+			# now to update the label
+			for attribute in res['results'][0]['attributes']:
+				if attribute['name'] == "cssClass":
+					if machine['authUserInUserOwns'] and machine['authUserInRootOwns']:
+						ea.patch_attribute(attributeId=attribute['attributeId'], value="done")
+					else:
+						if machine['authUserInUserOwns'] or machine['authUserInRootOwns']:
+							ea.patch_attribute(attributeId=attribute['attributeId'], value="inprogress")
+						else:
+							ea.patch_attribute(attributeId=attribute['attributeId'], value="todo")
+							
+	else:
+		# title does not exist - create the note
+		html = generate_newpage(machine)
+		new_note = ea.create_note(
+			parentNoteId=trilium_machines_htb_folder,
+			type="text",
+			title=machine['name'],
+			content=html,
+		)
+		print("[+] created note:", machine['name'], "                        ")
+
+		ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="relation", name="template", value=trilium_machines_htb_folder)
+		if machine['authUserInUserOwns'] and machine['authUserInRootOwns']:
+			ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="done")
+		else:
+			if machine['authUserInUserOwns'] or machine['authUserInRootOwns']:
+				ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="inprogress")
+			else:
+				ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="todo")
+
+	if machine['authUserInUserOwns'] and machine['authUserInRootOwns']:
+			completed_count += 1
+
+print("[+] updating folder name                                          ")
+ea.patch_note(noteId=trilium_machines_htb_folder,title="Machines - "+str(completed_count)+" / "+str(len(machines)))
+
+print("[=] processed", machine_count)
\ No newline at end of file
diff --git a/htb2trilium/htb2trilium_sherlocks.py b/htb2trilium/htb2trilium_sherlocks.py
new file mode 100644
index 0000000..1e32e59
--- /dev/null
+++ b/htb2trilium/htb2trilium_sherlocks.py
@@ -0,0 +1,151 @@
+import os
+import json
+import requests
+from collections import defaultdict
+from bs4 import BeautifulSoup
+from datetime import datetime, timezone
+from htb_client import HTBClient
+from trilium_py.client import ETAPI
+
+# Get the absolute path of the script's directory
+script_dir = os.path.dirname(os.path.abspath(__file__))
+
+# Construct the full path to the config.json file
+config_path = os.path.join(script_dir, 'config.json')
+
+# Load configuration from the JSON file
+with open(config_path, 'r') as f:
+    config = json.load(f)
+
+# Accessing config values
+htb_code = config['htb_code']
+trilium_server_url = config['trilium_server_url']
+trilium_token = config['trilium_token']
+trilium_sherlocks_folder = config['trilium_sherlocks_folder']
+trilium_sherlocks_template_id = config['trilium_sherlocks_template_id']
+
+def get_timestamp(machine):
+    # Parse the release date string into a datetime object
+    release_str = machine['release_date']
+    dt = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ")
+    
+    # Set the timezone to UTC
+    dt = dt.replace(tzinfo=timezone.utc)
+    
+    # Get the timestamp
+    timestamp = dt.timestamp()
+    return timestamp
+
+print("[+] connecting to HTB")
+client = HTBClient(password=htb_code)
+print("[+] connecting to trilium")
+ea = ETAPI(trilium_server_url, trilium_token)
+print("[i] version: ", ea.app_info()['appVersion'])
+
+print("[i] HTB User:", client.user['id'], "-", client.user['name'])
+
+
+print("[+] gathering sherlocks info")
+categories = defaultdict(list)
+sherlocks = client.get_all_sherlocks()
+sherlocks.sort(key=get_timestamp)
+total_completed = 0  # Variable to track total completed sherlocks
+
+print(f"[i] Retrieved {len(sherlocks)} sherlocks")
+
+# Group sherlocks by their categories
+for sherlock in sherlocks:
+    categories[sherlock['category_name']].append(sherlock)
+    if sherlock['progress'] == 100:
+        total_completed += 1  # Increment total completed if challenge is owned
+
+# Print out the grouped sherlocks with the number of completed and total sherlocks in each category
+for category, grouped_sherlocks in categories.items():
+    total = len(grouped_sherlocks)
+    completed = sum(1 for sherlock in grouped_sherlocks if sherlock['progress'] == 100)  # Count completed challenges
+    
+    res = ea.search_note(
+        search=f"note.title %= '{category}*'",
+        ancestorNoteId=trilium_sherlocks_folder,
+        ancestorDepth='eq1',
+        limit=1,
+        fastSearch=True,
+    )
+    
+    catId = ""
+    if res['results'] and res['results'][0]['title'].split(' - ')[0].strip().lower() == category.lower():
+	# page exists - lets check if the details have changed
+        ea.patch_note(noteId=res['results'][0]['noteId'], title=category+" - "+str(completed)+" / "+str(total))
+        catId = res['results'][0]['noteId']
+        print(f"[i] updated category: {category} - ({completed}/{total})")
+    else:
+        new_note = ea.create_note(
+            parentNoteId=trilium_sherlocks_folder,
+            type="text",
+            title=category+" - "+str(completed)+" / "+str(total),
+            content=" ",
+        )
+        catId = new_note['note']['noteId']
+        print(f"[+] created category: {catId} {category} - ({completed}/{total})")
+    
+    for sherlock in grouped_sherlocks:
+        #print(f" - ID: {challenge['id']}, Name: {challenge['name']}, Difficulty: {challenge['difficulty']}")
+        res2 = ea.search_note(
+            search=f"{sherlock['name']}",
+            ancestorNoteId=catId,
+            ancestorDepth='eq1',
+            limit=1,
+            fastSearch=True,
+        )
+        # already exists update the values
+        if res2['results'] and res2['results'][0]['title'].lower() == sherlock['name'].lower():
+            print(f"[i] found ID: {sherlock['id']}, Name: {sherlock['name']}, Difficulty: {sherlock['difficulty']}")
+    
+            for attribute in res2['results'][0]['attributes']:
+                if attribute['name'] == "Difficulty":
+                    ea.patch_attribute(attributeId=attribute['attributeId'], value=sherlock['difficulty'])
+        
+                if attribute['name'] == "Released":
+                    release_str = sherlock['release_date']
+                    release_date = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ")
+                    formatted_release_date = release_date.strftime("%d %B %Y")
+                    ea.patch_attribute(attributeId=attribute['attributeId'], value=formatted_release_date)
+        
+                if attribute['name'] == "Solved":
+                    ea.patch_attribute(attributeId=attribute['attributeId'], value=str(sherlock['progress']))
+        
+                if attribute['name'] == "cssClass":
+                    # Determine the cssClass value based on the progress
+                    if sherlock['progress'] == 0:
+                        ea.patch_attribute(attributeId=attribute['attributeId'], value="todo")
+                    elif sherlock['progress'] == 100:
+                        ea.patch_attribute(attributeId=attribute['attributeId'], value="done")
+                    elif 0 < sherlock['progress'] < 100:
+                        ea.patch_attribute(attributeId=attribute['attributeId'], value="inprogress")
+
+
+        else: # doesnt already exist, create page
+            release_str = sherlock['release_date']
+            release_date = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ")
+            formatted_release_date = release_date.strftime("%d %B %Y")
+            new_note = ea.create_note(
+                parentNoteId=catId,
+                type="text",
+                title=sherlock['name'],
+                content=" ",
+            )
+            ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="relation", name="template", value=trilium_sherlocks_template_id)
+            ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Difficulty", value=sherlock['difficulty'])
+            ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Released", value=formatted_release_date)
+            ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Solved", value=str(sherlock['progress']))
+            if sherlock['progress'] == 0:
+                ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="todo")
+            elif sherlock['progress'] == 100:
+                ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="done")
+            elif 0 < sherlock['progress'] < 100:
+                ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="inprogress")
+
+            print(f"[+] created ID: {sherlock['id']}, Name: {sherlock['name']}, Difficulty: {sherlock['difficulty']}")
+
+print("[+] updating folder name                                          ")
+ea.patch_note(noteId=trilium_sherlocks_folder,title="Sherlocks - "+str(total_completed)+" / "+str(len(sherlocks)))
diff --git a/htb2trilium/htb_client.py b/htb2trilium/htb_client.py
new file mode 100644
index 0000000..a35929a
--- /dev/null
+++ b/htb2trilium/htb_client.py
@@ -0,0 +1,214 @@
+import requests
+import warnings
+
+warnings.filterwarnings("ignore")
+
+class HTBClient:
+    def __init__(self, password):
+        self.password = password
+        self.base_url = 'https://labs.hackthebox.com/api/v4'
+        self.proxies = {
+            #"http": "http://127.0.0.1:8080",  # Burp proxy for HTTP traffic
+            #"https": "http://127.0.0.1:8080"  # Burp proxy for HTTPS traffic
+        }
+        # Fetch user info
+        self.user = self.get_user_info()
+        
+        # Fetch user stats and store them in self.user
+        user_owns, root_owns, respects = self.get_user_stats(self.user['id'])
+        self.user['user_owns'] = user_owns
+        self.user['root_owns'] = root_owns
+        self.user['respects'] = respects
+
+    def get_user_info(self):
+        headers = {
+            "Authorization": f"Bearer {self.password}",
+            "User-Agent": None  # Explicitly remove User-Agent
+        }
+        response = requests.get(
+            f'{self.base_url}/user/info', 
+            headers=headers, 
+            proxies=self.proxies,
+            verify=False  # Disable SSL verification
+        )
+        if response.status_code != 200:
+            raise Exception(f"Error fetching {self.base_url}/user/info user info: {response.status_code}, {response.text}")
+        
+        # Return the user info as a dictionary
+        data = response.json().get('info')
+        return {'id': data['id'], 'name': data['name'], 'email': data['email']}
+
+    def get_user_stats(self, user_id):
+        headers = {
+            "Authorization": f"Bearer {self.password}",
+            "User-Agent": None  # Explicitly remove User-Agent
+        }
+        response = requests.get(
+            f'{self.base_url}/user/profile/basic/{user_id}', 
+            headers=headers, 
+            proxies=self.proxies,
+            verify=False  # Disable SSL verification
+        )
+        if response.status_code != 200:
+            raise Exception(f"Error fetching user stats: {response.status_code}, {response.text}")
+        
+        # Extract user statistics from the response
+        data = response.json().get('profile')
+        user_owns = data['user_owns']
+        root_owns = data['system_owns']
+        respects = data['respects']
+        return user_owns, root_owns, respects
+
+    def get_active_machines(self):
+        machines = []
+        seen_ids = set()  # Track unique machine IDs
+        seen_names = set()  # Track unique machine names
+        page = 1
+
+        while True:
+            response = requests.get(
+                f'{self.base_url}/machine/paginated?per_page=100&page={page}',
+                headers={
+                    "Authorization": f"Bearer {self.password}",
+                    "User-Agent": None  # Explicitly remove User-Agent
+                },
+                proxies=self.proxies,
+                verify=False  # Disable SSL verification
+            )
+
+            if response.status_code != 200:
+                raise Exception(f"Error fetching active machines: {response.status_code}, {response.text}")
+
+            data = response.json()
+            for machine in data['data']:
+                if machine['id'] not in seen_ids and machine['name'] not in seen_names:
+                    machines.append(machine)
+                    seen_ids.add(machine['id'])
+                    seen_names.add(machine['name'])
+
+            # Check for pagination
+            if page >= data['meta']['last_page']:
+                break
+            page += 1
+        
+        return machines
+
+    def get_retired_machines(self):
+        machines = []
+        seen_ids = set()  # Track unique machine IDs
+        seen_names = set()  # Track unique machine names
+        page = 1
+
+        while True:
+            response = requests.get(
+                f'{self.base_url}/machine/list/retired/paginated?per_page=100&page={page}',
+                headers={
+                    "Authorization": f"Bearer {self.password}",
+                    "User-Agent": None  # Explicitly remove User-Agent
+                },
+                proxies=self.proxies,
+                verify=False  # Disable SSL verification
+            )
+
+            if response.status_code != 200:
+                raise Exception(f"Error fetching retired machines: {response.status_code}, {response.text}")
+
+            data = response.json()
+            for machine in data['data']:
+                if machine['id'] not in seen_ids and machine['name'] not in seen_names:
+                    machines.append(machine)
+                    seen_ids.add(machine['id'])
+                    seen_names.add(machine['name'])
+
+            # Check for pagination
+            if page >= data['meta']['last_page']:
+                break
+            page += 1
+        
+        return machines
+
+    def get_all_machines(self):
+        # Combine active and retired machines, ensuring no duplicates
+        active_machines = self.get_active_machines()
+        retired_machines = self.get_retired_machines()
+
+        all_machines = active_machines + retired_machines
+        seen_ids = set()  # Track unique machine IDs
+        seen_names = set()  # Track unique machine names
+        unique_machines = []
+
+        for machine in all_machines:
+            if machine['id'] not in seen_ids and machine['name'] not in seen_names:
+                unique_machines.append(machine)
+                seen_ids.add(machine['id'])
+                seen_names.add(machine['name'])
+
+        return unique_machines
+
+    def get_all_challenges(self):
+        challenges = []
+        seen_ids = set()  # Track unique machine IDs
+        seen_names = set()  # Track unique machine names
+        page = 1
+
+        while True:
+            response = requests.get(
+                f'{self.base_url}/challenges?per_page=100&page={page}',
+                headers={
+                    "Authorization": f"Bearer {self.password}",
+                    "User-Agent": None  # Explicitly remove User-Agent
+                },
+                proxies=self.proxies,
+                verify=False  # Disable SSL verification
+            )
+
+            if response.status_code != 200:
+                raise Exception(f"Error fetching challenges: {response.status_code}, {response.text}")
+
+            data = response.json()
+            for challenge in data['data']:
+                if challenge['id'] not in seen_ids and challenge['name'] not in seen_names:
+                    challenges.append(challenge)
+                    seen_ids.add(challenge['id'])
+                    seen_names.add(challenge['name'])
+
+            # Check for pagination
+            if page >= data['meta']['last_page']:
+                break
+            page += 1
+        
+        return challenges
+
+    def get_all_sherlocks(self):
+        sherlocks = []
+        seen_ids = set()  # Track unique machine IDs
+        seen_names = set()  # Track unique machine names
+        page = 1
+
+        while True:
+            response = requests.get(
+                f'{self.base_url}/sherlocks?per_page=100&page={page}',
+                headers={
+                    "Authorization": f"Bearer {self.password}",
+                    "User-Agent": None  # Explicitly remove User-Agent
+                },
+                proxies=self.proxies,
+                verify=False  # Disable SSL verification
+            )
+
+            if response.status_code != 200:
+                raise Exception(f"Error fetching sherlocks: {response.status_code}, {response.text}")
+
+            data = response.json()
+            for sherlock in data['data']:
+                if sherlock['id'] not in seen_ids and sherlock['name'] not in seen_names:
+                    sherlocks.append(sherlock)
+                    seen_ids.add(sherlock['id'])
+                    seen_names.add(sherlock['name'])
+
+            # Check for pagination
+            if page >= data['meta']['last_page']:
+                break
+            page += 1
+        
+        return sherlocks
\ No newline at end of file
diff --git a/htb2trilium/images/machine_view.png b/htb2trilium/images/machine_view.png
new file mode 100755
index 0000000..5bcb448
--- /dev/null
+++ b/htb2trilium/images/machine_view.png
Binary files differ

diff --git a/README.md b/README.md
index f623b41..fefccb6 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,58 @@
 TriliumScripts
 ===============
 
-My collection of scripts for use with trilium
\ No newline at end of file
+Collection of scripts for use with trilium
+
+## syntax highlighting
+
+**highlight_moodlog.js** & **highlight_todo.js** - Both of these will apply appropriate syntaxy highlighting to the specific note.
+
+### installation
+
+1) add the code as the notes contents.
+
+2) set "note type" to "JS frontend"
+
+3) add an "owned attribute" of "#widget"
+
+### use
+
+on the note you want highlighting set the "owned attributes" to "#todotxt #hideHighlightWidget" or "#moodlog #hideHighlightWidget" depending on which syntax type you want to highlight
+
+## bidirectional sync
+
+**trilium_sync.php** - script to keep a folder of local text files and a trilium note in sync.
+
+### installation
+
+Edit the config values at the top of the script to match your trilium setup. Run it manually to test it is working as expected. If it is create a cron job.
+
+### example use
+
+```
+$> php trilium_sync.php 
+[i] checking connections
+[i] version: x.xx.x
+[i] gathering info
+
+- Folders -
+Folder/Path              Folder Name              Parent       noteId       Mod Trl    Mod Dsk    Status         
+Synced/                  Synced                   root         j0DRZhIzV97W 1742502936 1742507367 Exists
+Synced/BothPlaces/       BothPlaces               j0DRZhIzV97W AQb3Dg0yZHSu 1742507075 1742507200 Exists
+Synced/[del] tobe del/   [del] tobe del           j0DRZhIzV97W j0xUs1FdZL1Y 1742507321            Missing_Disk   
+Synced/AddToTrilium/     AddToTrilium                                                  1742507383 Missing_Trilium
+Synced/tobe deleted/     tobe deleted                                                  1742507200 Missing_Trilium
+- Files -
+Folder/Path              Filename                 Parent       noteId       Mod Trl    Mod Dsk    Hash  Status         
+Synced/BothPlaces/       UpdateDiskFile           AQb3Dg0yZHSu IwUZLwWdbiWG 1742507314 1742507199 Diff  Update_Disk    
+Synced/BothPlaces/       UpdateTriliumFile        AQb3Dg0yZHSu 7pMPgsb9AVRf 1742507130 1742507345 Diff  Update_Trilium 
+Synced/[del] tobe del/   gone_also.txt            j0xUs1FdZL1Y 4wOoHDy07csr 1742507178            N/A   Missing_Disk   
+Synced/AddToTrilium/     fromDiskToTrilium.txt                                         1742507394 N/A   Missing_Trilium
+Synced/tobe deleted/     gone_also.txt                                                 1742507200 N/A   Missing_Trilium
+
+[-] deleting disk folder: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/tobe deleted/
+[+] creating trilium folder: Synced/AddToTrilium/
+[+] creating trilium note: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/AddToTrilium/fromDiskToTrilium.txt
+[+] updating file on disk: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/BothPlaces/UpdateDiskFile
+[+] updating file on trilium: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/BothPlaces/UpdateTriliumFile
+```
\ No newline at end of file
diff --git a/highlight_moodlog.js b/highlight_moodlog.js
new file mode 100644
index 0000000..be4c283
--- /dev/null
+++ b/highlight_moodlog.js
@@ -0,0 +1,165 @@
+const TPL = `<div style="padding: 10px; border-top: 1px solid var(--main-border-color); contain: none;">
+    <strong>Moodlog Highlighting Applied</strong>
+</div>`;
+
+class MoodlogHighlighterWidget extends api.NoteContextAwareWidget {
+    static get parentWidget() { 
+        //console.log("MDLG: Getting parent widget: center-pane");
+        return 'center-pane'; 
+    }
+
+    get position() { 
+        //console.log("MDLG: Setting position: 100");
+        return 100; 
+    }
+
+    isEnabled() {
+        const enabled = super.isEnabled() 
+            && this.note.type === 'text' 
+            && this.note.hasLabel('moodlog');
+        //console.log(`MDLG: isEnabled: ${enabled}`);
+        return enabled;
+    }
+
+    doRender() {
+        //console.log("MDLG: Rendering widget...");
+        this.$widget = $(TPL);
+        //console.log("MDLG: Widget rendered:", this.$widget);
+        return this.$widget;
+    }
+
+    async refreshWithNote(note) {
+        //console.log("MDLG: Refreshing with note:", note);
+        const { content } = await note.getNoteComplement();
+        //console.log("MDLG: Note content fetched:", content);
+
+        // Check if content is fetched properly
+        if (!content) {
+            //console.log("MDLG: No content fetched. Exiting refresh.");
+            return;
+        }
+
+        const highlightedContent = this.applyHighlighting(content);
+        //console.log("MDLG: Highlighted content:", highlightedContent);
+
+        if (highlightedContent !== content) {
+            //console.log("MDLG: Updating note content with highlighted version");
+
+            // Use the setNoteContent function to update the note content
+            try {
+                await setNoteContent(note.noteId, highlightedContent);
+                //console.log("MDLG: Content updated with highlighted version");
+            } catch (error) {
+                //console.error("MDLG: Error updating content:", error);
+            }
+        } else {
+            //console.log("MDLG: No changes to content, skipping update.");
+        }
+    }
+
+
+ applyHighlighting(content) {
+    //console.log("MDLG: Applying highlighting to content...");
+
+    // Replace <br> with \n, then remove all other HTML tags
+    const plainTextContent = content.replace(/<br\s*\/?>/gi, '\n').replace(/<[^>]*>/g, ' ');
+    //console.log("MDLG: Stripped content:", plainTextContent);
+
+    // Combined regex for date & time
+    const dateTimePattern = /([1-2]\d{3}[-/.](0?[1-9]|1[0-2])[-/.](0[1-9]|[12]\d|3[01]))\s+(\d{1,2}:\d{2})/g;
+    const projectPattern = /\[\S+?\]/g;
+    const contextPattern = /(\s|^)(@\S+)/g;
+    const hashtagPattern = /(\s|^)(#\S+)/g;
+    const positivePattern = /(\s|^)(\+\S+)/g;
+    const negativePattern = /(\s|^)(-\S+)/g;
+
+    // Escape HTML characters to prevent issues with replacements
+    function escapeHtml(str) {
+        return str.replace(/[&<>"']/g, function (char) {
+            return {
+                '&': '&amp;',
+                '<': '&lt;',
+                '>': '&gt;',
+                '"': '&quot;',
+                "'": '&#039;'
+            }[char];
+        });
+    }
+
+      // Helper function to map the number to a colour based on the gradient
+    function getProjectColour(number) {
+        const minColor = { r: 107, g: 109, b: 229 };  // #6b6de5
+        const maxColor = { r: 70, g: 226, b: 96 };   // #46e260
+
+        const ratio = number / 9; // since we are working with 0-9
+
+        // Interpolate the RGB values between minColor and maxColor
+        const r = Math.round(minColor.r + ratio * (maxColor.r - minColor.r));
+        const g = Math.round(minColor.g + ratio * (maxColor.g - minColor.g));
+        const b = Math.round(minColor.b + ratio * (maxColor.b - minColor.b));
+
+        return `rgb(${r}, ${g}, ${b})`;
+    }
+     
+    // Process each line of the plain text content
+    let highlightedLines = plainTextContent.split("\n").map(line => {
+        //console.log("MDLG: Processing line:", line);
+
+        // Apply combined date & time highlight
+        line = line.replace(dateTimePattern, `<span style="color: rgb(188, 55, 237);">$1</span> <span style="color: rgb(188, 55, 237);">$4</span>`);
+
+        // Apply other highlights
+        line = line.replace(projectPattern, (match) => {
+            // Extract the number inside square brackets
+            const number = parseInt(match.match(/\d+/)[0], 10);
+            const colour = getProjectColour(number);
+            return `<span style="color: ${colour}; font-weight: bold;">${escapeHtml(match)}</span>`;
+        });
+        
+        // Apply other highlights
+        //line = line.replace(projectPattern, `<span style="color: rgb(224, 218, 36); font-weight: bold;">$&</span>`);
+        line = line.replace(contextPattern, (_, space, tag) => `${space}<span style="color: rgb(255 165 10);">${escapeHtml(tag)}</span>`);
+        line = line.replace(hashtagPattern, (_, space, tag) => `${space}<span style="color: rgb(41, 229, 242);">${escapeHtml(tag)}</span>`);
+        line = line.replace(positivePattern, (_, space, tag) => `${space}<span style="color: rgb(44, 227, 34);">${escapeHtml(tag)}</span>`);
+        line = line.replace(negativePattern, (_, space, tag) => `${space}<span style="color: rgb(230, 59, 44);">${escapeHtml(tag)}</span>`);
+
+        return line.trim();
+    });
+
+    // Join lines with proper <br> handling
+    let finalContent = highlightedLines.join("<br>");
+
+    //console.log("MDLG: Highlighted content:", finalContent);
+    return finalContent;
+}
+
+
+
+
+    async entitiesReloadedEvent({ loadResults }) {
+        //console.log("MDLG: Entities reloaded event triggered");
+        if (loadResults.isNoteContentReloaded(this.noteId)) {
+            //console.log("MDLG: Note content reloaded, refreshing widget...");
+            //this.refresh();
+        } else {
+            //console.log("MDLG: No content reload detected.");
+        }
+    }
+    
+
+}
+
+console.log("MDLG: Starting Moodlog Highlighter Widget");
+module.exports = MoodlogHighlighterWidget;
+
+function setNoteContent(noteId, content) {
+    /*const noteElement = document.querySelector(`note-detail-readonly-text-content`);
+
+    if (noteElement) {
+        noteElement.innerHTML = content; // Update the displayed content
+        console.log(`MDLG: Updated note ${noteId} in the DOM.`);
+    } else {
+        console.warn(`MDLG: Note element with data-note-id='${noteId}' not found.`);
+    }*/
+    return api.runOnBackend((id, data) => api.getNote(id).setContent(data), [noteId, content]);
+}
diff --git a/highlight_todotxt.js b/highlight_todotxt.js
new file mode 100644
index 0000000..1f1b007
--- /dev/null
+++ b/highlight_todotxt.js
@@ -0,0 +1,157 @@
+const TPL = `<div style="padding: 10px; border-top: 1px solid var(--main-border-color); contain: none;">
+    <strong>Todo Highlighting Applied</strong>
+</div>`;
+
+class MoodlogHighlighterWidget extends api.NoteContextAwareWidget {
+    static get parentWidget() { 
+        //console.log("MDLG: Getting parent widget: center-pane");
+        return 'center-pane'; 
+    }
+
+    get position() { 
+        //console.log("MDLG: Setting position: 100");
+        return 100; 
+    }
+
+    isEnabled() {
+        const enabled = super.isEnabled() 
+            && this.note.type === 'text' 
+            && this.note.hasLabel('todotxt');
+        //console.log(`MDLG: isEnabled: ${enabled}`);
+        return enabled;
+    }
+
+    doRender() {
+        //console.log("MDLG: Rendering widget...");
+        this.$widget = $(TPL);
+        //console.log("MDLG: Widget rendered:", this.$widget);
+        return this.$widget;
+    }
+
+    async refreshWithNote(note) {
+        //console.log("MDLG: Refreshing with note:", note);
+        const { content } = await note.getNoteComplement();
+        //console.log("MDLG: Note content fetched:", content);
+
+        // Check if content is fetched properly
+        if (!content) {
+            //console.log("MDLG: No content fetched. Exiting refresh.");
+            return;
+        }
+
+        const highlightedContent = this.applyHighlighting(content);
+        //console.log("MDLG: Highlighted content:", highlightedContent);
+
+        if (highlightedContent !== content) {
+            //console.log("MDLG: Updating note content with highlighted version");
+
+            // Use the setNoteContent function to update the note content
+            try {
+                await setNoteContent(note.noteId, highlightedContent);
+                //console.log("MDLG: Content updated with highlighted version");
+            } catch (error) {
+                //console.error("MDLG: Error updating content:", error);
+            }
+        } else {
+            //console.log("MDLG: No changes to content, skipping update.");
+        }
+    }
+
+
+applyHighlighting(content) {
+    // Replace <br> with \n, then remove all other HTML tags
+    const plainTextContent = content.replace(/<br\s*\/?\>/gi, '\n').replace(/<[^>]*>/g, ' ');
+
+    // Regular expressions for Todo.txt syntax
+    const datePattern = /\b(0[1-9]|[12]\d|3[01])\/(0[1-9]|1[0-2])\/([1-2]\d{3})\b/g;
+    const completedTaskPattern = /^\s*x\s+.*/g;
+    const priorityPattern = /^(\s*\([A-E]\))\s+/;
+    const projectPattern = /(?:\s|^)(\+\S+)/g;
+    const contextPattern = /(?:\s|^)(@\S+)/g;
+    const waitPattern = /\bWAIT\b/gi;
+    const customAttributePattern = /(\s+[^\s:]+:[^\s:]+)+\s*$/g;
+    
+    function escapeHtml(str) {
+        return str.replace(/[&<>"']/g, char => ({
+            '&': '&amp;', '<': '&lt;', '>': '&gt;',
+            '"': '&quot;', "'": '&#039;'
+        })[char]);
+    }
+
+    let highlightedLines = plainTextContent.split("\n").map(line => {
+        // If line starts with 'x', make the whole line grey
+        if (/^\s*x\s+/.test(line)) {
+            return `<span style="color: grey;">${line}</span>`;
+        }
+        
+        // Highlight priority with different colours based on level
+        line = line.replace(priorityPattern, (match, priority) => {
+            const level = priority.match(/[A-E]/)?.[0];
+            const colors = {
+                'A': 'rgb(255, 0, 0)',       // Red
+                'B': 'rgb(255, 102, 0)',     // Orange
+                'C': 'rgb(255, 204, 0)',     // Yellow
+                'D': 'rgb(0, 153, 255)',     // Blue
+                'E': 'rgb(102, 204, 102)'    // Green
+            };
+            const color = colors[level] || 'black';
+            return `<span style="color: ${color}; font-weight: bold;">${escapeHtml(priority)}</span> `;
+        });
+        
+        // Highlight date
+        line = line.replace(datePattern, (match) => {
+            return `<span style="color: rgb(188, 55, 237);">${escapeHtml(match)}</span>`;
+        });
+
+        // Highlight project tags
+        line = line.replace(projectPattern, (_, tag) => `<span style="color: rgb(224, 218, 36);"> ${escapeHtml(tag)}</span>`);
+        
+        // Highlight context tags
+        line = line.replace(contextPattern, (_, tag) => `<span style="color: rgb(255, 165, 10);"> ${escapeHtml(tag)}</span>`);
+        
+        // Highlight WAIT command
+        line = line.replace(waitPattern, `<span style="color: rgb(255, 69, 0);">WAIT</span>`);
+        
+        // Highlight custom attributes
+        line = line.replace(customAttributePattern, `<span style="color: rgb(100, 149, 237);">$1</span>`);
+        
+        // Remove leading space (if any) from the beginning of the line, without affecting spaces before + or @
+        line = line.replace(/^\s+/, '');
+        
+        return line.trim();
+    });
+
+    return highlightedLines.join("<br>");
+}
+
+
+
+
+
+    async entitiesReloadedEvent({ loadResults }) {
+        //console.log("MDLG: Entities reloaded event triggered");
+        if (loadResults.isNoteContentReloaded(this.noteId)) {
+            //console.log("MDLG: Note content reloaded, refreshing widget...");
+            //this.refresh();
+        } else {
+            //console.log("MDLG: No content reload detected.");
+        }
+    }
+    
+
+}
+
+console.log("TodoHighlight: Starting todotxt Highlighter Widget");
+module.exports = MoodlogHighlighterWidget;
+
+function setNoteContent(noteId, content) {
+    /*const noteElement = document.querySelector(`note-detail-readonly-text-content`);
+
+    if (noteElement) {
+        noteElement.innerHTML = content; // Update the displayed content
+        console.log(`MDLG: Updated note ${noteId} in the DOM.`);
+    } else {
+        console.warn(`MDLG: Note element with data-note-id='${noteId}' not found.`);
+    }*/
+    return api.runOnBackend((id, data) => api.getNote(id).setContent(data), [noteId, content]);
+}
diff --git a/htb2trilium/INSTALL.md b/htb2trilium/INSTALL.md
new file mode 100644
index 0000000..da8afad
--- /dev/null
+++ b/htb2trilium/INSTALL.md
@@ -0,0 +1,41 @@
+## scripts require:
+
+```
+ > pip3 install trilium-py
+```
+
+## config.json
+- get trilium token in trilium options->ETAPI and "create new token"
+- create a new note with a title "Machines"
+- click on this note -> "note info". Note ID is there. This ID goes in "trilium_machines_htb_folder" in the config file
+- set the folder's owned attributes to:
+
+```
+#label:user=promoted,single,text #label:root=promoted,single,text #label:respect=promoted,single,text #user=0 #root=0 #respect=0 
+```
+
+ - create a note named "HTBMachineTemplate" 
+ - set it's "owned attributes" to:
+
+```
+#template #label:User=promoted,single,text #label:Root=promoted,single,text #label:Tags=promoted,single,text
+```
+
+- get this page's ID and put in "trilium_machines_template_id"
+- create "challenges" page, this page's ID goes in: trilium_challenges_folder  
+- create "Sherlocks" page, this page's ID goes in: trilium_sherlocks_folder  
+- create 2 pages "HTBChallengesTemplate" and "HTBSherlocksTemplate", both of these should have the owned attributes:
+
+```
+#template #label:Difficulty=promoted,single,text #label:Solved=promoted,single,text #label:Released=promoted,single,text 
+```
+
+- The ID's of "HTBChallengesTemplate" and "HTBSherlocksTemplate" go in the matching config values.
+
+- to get "in progress" todo colour - add to trilium demo/scripting/taskmanager/implementation/CSS:
+
+```
+ span.fancytree-node.inprogress .fancytree-title {
+    color: orange !important;
+ }
+```
\ No newline at end of file
diff --git a/htb2trilium/README.md b/htb2trilium/README.md
new file mode 100644
index 0000000..1b45ae2
--- /dev/null
+++ b/htb2trilium/README.md
@@ -0,0 +1,16 @@
+htb2trilium
+===============
+
+Pull hackthebox info and stats into personal trilium note taking software
+
+single machine view:
+
+![machine view](images/machine_view.png)
+
+overview of machines:
+
+![machines overview](images/machines_overview.png)
+
+navigation:
+
+![navigation](images/navigation.png)
\ No newline at end of file
diff --git a/htb2trilium/config.json b/htb2trilium/config.json
new file mode 100644
index 0000000..39597ce
--- /dev/null
+++ b/htb2trilium/config.json
@@ -0,0 +1,11 @@
+{
+	"htb_code": "in https://app.hackthebox.com/profile/settings click 'profile settings' and under 'app tokens' you can 'create app token' - that goes here",
+	"trilium_server_url": "https://place.to.your.trilium.server",
+	"trilium_token": "",
+	"trilium_machines_htb_folder": "",
+	"trilium_machines_template_id": "",
+	"trilium_challenges_folder": "",
+	"trilium_challenges_template_id": "",
+	"trilium_sherlocks_folder": "",
+	"trilium_sherlocks_template_id": ""
+}
\ No newline at end of file
diff --git a/htb2trilium/htb2trilium_challenges.py b/htb2trilium/htb2trilium_challenges.py
new file mode 100644
index 0000000..d0d95ca
--- /dev/null
+++ b/htb2trilium/htb2trilium_challenges.py
@@ -0,0 +1,146 @@
+import os
+import json
+import requests
+from collections import defaultdict
+from bs4 import BeautifulSoup
+from datetime import datetime, timezone
+from htb_client import HTBClient
+from trilium_py.client import ETAPI
+
+# Get the absolute path of the script's directory
+script_dir = os.path.dirname(os.path.abspath(__file__))
+
+# Construct the full path to the config.json file
+config_path = os.path.join(script_dir, 'config.json')
+
+# Load configuration from the JSON file
+with open(config_path, 'r') as f:
+    config = json.load(f)
+
+# Accessing config values
+htb_code = config['htb_code']
+trilium_server_url = config['trilium_server_url']
+trilium_token = config['trilium_token']
+trilium_challenges_folder = config['trilium_challenges_folder']
+trilium_challenges_template_id = config['trilium_challenges_template_id']
+
+def get_timestamp(machine):
+    # Parse the release date string into a datetime object
+    release_str = machine['release_date']
+    dt = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ")
+    
+    # Set the timezone to UTC
+    dt = dt.replace(tzinfo=timezone.utc)
+    
+    # Get the timestamp
+    timestamp = dt.timestamp()
+    return timestamp
+
+print("[+] connecting to HTB")
+client = HTBClient(password=htb_code)
+print("[+] connecting to trilium")
+ea = ETAPI(trilium_server_url, trilium_token)
+print("[i] version: ", ea.app_info()['appVersion'])
+
+print("[i] HTB User:", client.user['id'], "-", client.user['name'])
+
+
+print("[+] gathering challenges info")
+categories = defaultdict(list)
+challenges = client.get_all_challenges()
+challenges.sort(key=get_timestamp)
+total_completed = 0  # Variable to track total completed challenges
+
+print(f"[i] Retrieved {len(challenges)} challenges")
+
+# Group challenges by their categories
+for challenge in challenges:
+    categories[challenge['category_name']].append(challenge)
+    if challenge['is_owned']:
+        total_completed += 1  # Increment total completed if challenge is owned
+
+# Print out the grouped challenges with the number of completed and total challenges in each category
+for category, grouped_challenges in categories.items():
+    total = len(grouped_challenges)
+    completed = sum(1 for challenge in grouped_challenges if challenge['is_owned'])  # Count completed challenges
+    
+    res = ea.search_note(
+        search=f"note.title %= '{category}*'",
+        ancestorNoteId=trilium_challenges_folder,
+        ancestorDepth='eq1',
+        limit=1,
+        fastSearch=True,
+    )
+    
+    catId = ""
+    if res['results'] and res['results'][0]['title'].split(' - ')[0].strip().lower() == category.lower():
+        # page exists - lets check if the details have changed
+        ea.patch_note(noteId=res['results'][0]['noteId'], title=category+" - "+str(completed)+" / "+str(total))
+        catId = res['results'][0]['noteId']
+        print(f"[i] updated category: {category} - ({completed}/{total})")
+    else:
+        new_note = ea.create_note(
+            parentNoteId=trilium_challenges_folder,
+            type="text",
+            title=category+" - "+str(completed)+" / "+str(total),
+            content=" ",
+        )
+        catId = new_note['note']['noteId']
+        print(f"[+] created category: {catId} {category} - ({completed}/{total})")
+    
+    for challenge in grouped_challenges:
+        #print(f" - ID: {challenge['id']}, Name: {challenge['name']}, Difficulty: {challenge['difficulty']}")
+        res2 = ea.search_note(
+            search=f"{challenge['name']}",
+            ancestorNoteId=catId,
+            ancestorDepth='eq1',
+            limit=1,
+            fastSearch=True,
+        )
+        # already exists update the values
+        if res2['results'] and res2['results'][0]['title'].lower() == challenge['name'].lower():
+            print(f"[i] found ID: {challenge['id']}, Name: {challenge['name']}, Difficulty: {challenge['difficulty']}")
+            #print(f"Search response for challenge '{challenge['name']}': {res2}")
+            for attribute in res2['results'][0]['attributes']:
+                if attribute['name'] == "Difficulty":
+                    ea.patch_attribute(attributeId=attribute['attributeId'], value=challenge['difficulty'])
+                if attribute['name'] == "Released":
+                    release_str = challenge['release_date']
+                    release_date = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ")
+                    formatted_release_date = release_date.strftime("%d %B %Y")
+                    ea.patch_attribute(attributeId=attribute['attributeId'], value=formatted_release_date)
+                if attribute['name'] == "Solved":
+                    if challenge['is_owned']:
+                        ea.patch_attribute(attributeId=attribute['attributeId'], value="done")
+                    else:
+                        ea.patch_attribute(attributeId=attribute['attributeId'], value=" ")
+                if attribute['name'] == "cssClass":
+                    if challenge['is_owned']:
+                        ea.patch_attribute(attributeId=attribute['attributeId'], value="done")
+                    else:
+                        ea.patch_attribute(attributeId=attribute['attributeId'], value="todo")
+
+        else: # doesnt already exist, create page
+            release_str = challenge['release_date']
+            release_date = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ")
+            formatted_release_date = release_date.strftime("%d %B %Y")
+            new_note = ea.create_note(
+                parentNoteId=catId,
+                type="text",
+                title=challenge['name'],
+                content=" ",
+            )
+            ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="relation", name="template", value=trilium_challenges_template_id)
+            ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Difficulty", value=challenge['difficulty'])
+            ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Released", value=formatted_release_date)
+            if challenge['is_owned']:
+                ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="done")
+                ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Solved", value="done")
+            else:
+                ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="todo")
+                ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Solved", value=" ")
+
+            print(f"[+] created ID: {challenge['id']}, Name: {challenge['name']}, Difficulty: {challenge['difficulty']}")
+
+print("[+] updating folder name                                          ")
+ea.patch_note(noteId=trilium_challenges_folder,title="Challenges - "+str(total_completed)+" / "+str(len(challenges)))
diff --git a/htb2trilium/htb2trilium_machines.py b/htb2trilium/htb2trilium_machines.py
new file mode 100644
index 0000000..de8be53
--- /dev/null
+++ b/htb2trilium/htb2trilium_machines.py
@@ -0,0 +1,203 @@
+import os
+import json
+import requests
+from bs4 import BeautifulSoup
+from datetime import datetime, timezone
+from htb_client import HTBClient
+from trilium_py.client import ETAPI
+
+# Get the absolute path of the script's directory
+script_dir = os.path.dirname(os.path.abspath(__file__))
+
+# Construct the full path to the config.json file
+config_path = os.path.join(script_dir, 'config.json')
+
+# Load configuration from the JSON file
+with open(config_path, 'r') as f:
+    config = json.load(f)
+
+# Accessing config values
+htb_code = config['htb_code']
+trilium_server_url = config['trilium_server_url']
+trilium_token = config['trilium_token']
+trilium_machines_htb_folder = config['trilium_machines_htb_folder']
+trilium_machines_template_id = config['trilium_machines_template_id']
+
+def generate_newpage(machine):
+	no = "<span style=\"color:hsl(0,75%,60%);\">No</span>"
+	yes = "<span style=\"color:hsl(120,75%,60%);\">Yes</span>"
+	user_colour = no
+	if machine['authUserInUserOwns']:
+		user_colour = yes
+	root_colour = no
+	if machine['authUserInRootOwns']:
+		root_colour = yes
+
+	status = "Retired"
+	if machine['active']:
+		status = "Active"
+
+	release_str = machine['release']
+	release_date = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ")
+	formatted_release_date = release_date.strftime("%d %B %Y")
+
+	html = """
+	<figure class="image image-style-align-left image_resized" style="width:12.28%;">
+	  <img src="https://www.hackthebox.com{avatar}">
+	</figure>
+	<p>
+	  <strong>OS:</strong> {os}<br>
+	  <strong>Difficulty:</strong> {difficultyText} <br>
+	  <strong>Rating:</strong> {rating} / 5<br>
+	  <strong>Points:</strong> {points}<br>
+	  <strong>User / Root: </strong> {user_colour} / {root_colour}<br>
+	  <strong>Released:</strong> {release_date}<br>
+	  <strong>State:</strong> {status}
+	</p>
+	<hr>
+	<h2>Notes</h2>
+	<p>&nbsp;</p>
+	""".format(
+		os=machine['os'],
+		difficultyText=machine['difficultyText'],
+		rating=machine['star'],
+		points=machine['points'],
+		release_date=formatted_release_date,
+		user_colour=user_colour,
+		root_colour=root_colour,
+		status = status,
+		avatar = machine['avatar'],
+	)
+
+	return html
+
+def get_timestamp(machine):
+    # Parse the release date string into a datetime object
+    release_str = machine['release']
+    dt = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ")
+    
+    # Set the timezone to UTC
+    dt = dt.replace(tzinfo=timezone.utc)
+    
+    # Get the timestamp
+    timestamp = dt.timestamp()
+    return timestamp
+
+print("[+] connecting to HTB")
+client = HTBClient(password=htb_code)
+print("[+] connecting to trilium")
+ea = ETAPI(trilium_server_url, trilium_token)
+print("[i] version: ", ea.app_info()['appVersion'])
+
+print("[i] HTB User:", client.user['id'], "-", client.user['name'])
+print("[i] user owns:", client.user['user_owns'], "| Root owns:", client.user['root_owns'], "| Respect:", client.user['respects'])
+
+master_folder = ea.get_note(trilium_machines_htb_folder)
+for attribute in master_folder['attributes']:
+	if attribute['name'] == "user":
+		if attribute['value'] != str(client.user['user_owns']):
+			print("[+] updating user owns (folder attribute)")
+			ea.patch_attribute(attributeId=attribute['attributeId'], value=str(client.user['user_owns']))
+	if attribute['name'] == "root":
+		if attribute['value'] != str(client.user['root_owns']):
+			print("[+] updating root owns (folder attribute)")
+			ea.patch_attribute(attributeId=attribute['attributeId'], value=str(client.user['root_owns']))
+	if attribute['name'] == "respect":
+		if attribute['value'] != str(client.user['respects']):
+			print("[+] updating respect (folder attribute)")
+			ea.patch_attribute(attributeId=attribute['attributeId'], value=str(client.user['respects']))
+
+print("[+] gathering machines info")
+machines = client.get_all_machines()
+machines.sort(key=get_timestamp)
+
+print(f"[i] Retrieved {len(machines)} machines")
+#for machine in machines:
+#    print(f" - ID: {machine['id']}, Name: {machine['name']}, OS: {machine['os']}, Difficulty: {machine['difficultyText']}")
+
+machine_count = 0
+completed_count = 0
+for machine in machines:
+	machine_count += 1
+	print('processing: ',machine_count, "/", len(machines), "("+machine['name']+")               " , end='\r')
+
+	res = ea.search_note(
+		search="\""+machine['name']+"\"",
+		ancestorNoteId=trilium_machines_htb_folder,
+		ancestorDepth='eq1',
+		limit=1,
+		fastSearch=True,
+	)
+
+	if res['results'] and machine['name'] == res['results'][0]['title']:
+		# page exists - lets check if the details have changed
+
+		current_html = ea.get_note_content(noteId=res['results'][0]['noteId'])
+		current_soup = BeautifulSoup(current_html, 'html.parser')
+		current_paragraph = current_soup.find_all('p')[0].text
+
+		new_html = generate_newpage(machine)
+		new_soup = BeautifulSoup(new_html, 'html.parser')
+		new_paragraph = new_soup.find_all('p')[0].text
+
+		# current page contains first paragraph of "blank" (useful for when it doesnt create or find the note properly.. shouldnt get here)
+		if current_paragraph == "blank":
+			ea.update_note_content(noteId=res['results'][0]['noteId'], content=new_html)
+			ea.create_attribute(attributeId=None, isInheritable=False, noteId=res['results'][0]['noteId'], type="relation", name="template", value=trilium_machines_htb_folder)
+			if machine['authUserInUserOwns'] and machine['authUserInRootOwns']:
+				ea.create_attribute(attributeId=None, isInheritable=False, noteId=res['results'][0]['noteId'], type="label", name="cssClass", value="done")
+			else:
+				if machine['authUserInUserOwns'] or machine['authUserInRootOwns']:
+					ea.create_attribute(attributeId=None, isInheritable=False, noteId=res['results'][0]['noteId'], type="label", name="cssClass", value="inprogress")
+				else:
+					ea.create_attribute(attributeId=None, isInheritable=False, noteId=res['results'][0]['noteId'], type="label", name="cssClass", value="todo")
+			# re-get the current content
+			current_html = ea.get_note_content(noteId=res['results'][0]['noteId'])
+			current_soup = BeautifulSoup(current_html, 'html.parser')
+
+		if current_paragraph != new_paragraph:
+
+			# details have updated!
+			print("[+] updating page:",machine['name'], "-> "+res['results'][0]['title']+"                 ")
+			replacement = current_soup.find('p')
+			replacement.replace_with( new_soup.find_all('p')[0] )
+			ea.update_note_content(noteId=res['results'][0]['noteId'], content=current_soup)
+
+			# now to update the label
+			for attribute in res['results'][0]['attributes']:
+				if attribute['name'] == "cssClass":
+					if machine['authUserInUserOwns'] and machine['authUserInRootOwns']:
+						ea.patch_attribute(attributeId=attribute['attributeId'], value="done")
+					else:
+						if machine['authUserInUserOwns'] or machine['authUserInRootOwns']:
+							ea.patch_attribute(attributeId=attribute['attributeId'], value="inprogress")
+						else:
+							ea.patch_attribute(attributeId=attribute['attributeId'], value="todo")
+							
+	else:
+		# title does not exist - create the note
+		html = generate_newpage(machine)
+		new_note = ea.create_note(
+			parentNoteId=trilium_machines_htb_folder,
+			type="text",
+			title=machine['name'],
+			content=html,
+		)
+		print("[+] created note:", machine['name'], "                        ")
+
+		ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="relation", name="template", value=trilium_machines_htb_folder)
+		if machine['authUserInUserOwns'] and machine['authUserInRootOwns']:
+			ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="done")
+		else:
+			if machine['authUserInUserOwns'] or machine['authUserInRootOwns']:
+				ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="inprogress")
+			else:
+				ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="todo")
+
+	if machine['authUserInUserOwns'] and machine['authUserInRootOwns']:
+			completed_count += 1
+
+print("[+] updating folder name                                          ")
+ea.patch_note(noteId=trilium_machines_htb_folder,title="Machines - "+str(completed_count)+" / "+str(len(machines)))
+
+print("[=] processed", machine_count)
\ No newline at end of file
diff --git a/htb2trilium/htb2trilium_sherlocks.py b/htb2trilium/htb2trilium_sherlocks.py
new file mode 100644
index 0000000..1e32e59
--- /dev/null
+++ b/htb2trilium/htb2trilium_sherlocks.py
@@ -0,0 +1,151 @@
+import os
+import json
+import requests
+from collections import defaultdict
+from bs4 import BeautifulSoup
+from datetime import datetime, timezone
+from htb_client import HTBClient
+from trilium_py.client import ETAPI
+
+# Get the absolute path of the script's directory
+script_dir = os.path.dirname(os.path.abspath(__file__))
+
+# Construct the full path to the config.json file
+config_path = os.path.join(script_dir, 'config.json')
+
+# Load configuration from the JSON file
+with open(config_path, 'r') as f:
+    config = json.load(f)
+
+# Accessing config values
+htb_code = config['htb_code']
+trilium_server_url = config['trilium_server_url']
+trilium_token = config['trilium_token']
+trilium_sherlocks_folder = config['trilium_sherlocks_folder']
+trilium_sherlocks_template_id = config['trilium_sherlocks_template_id']
+
+def get_timestamp(machine):
+    # Parse the release date string into a datetime object
+    release_str = machine['release_date']
+    dt = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ")
+    
+    # Set the timezone to UTC
+    dt = dt.replace(tzinfo=timezone.utc)
+    
+    # Get the timestamp
+    timestamp = dt.timestamp()
+    return timestamp
+
+print("[+] connecting to HTB")
+client = HTBClient(password=htb_code)
+print("[+] connecting to trilium")
+ea = ETAPI(trilium_server_url, trilium_token)
+print("[i] version: ", ea.app_info()['appVersion'])
+
+print("[i] HTB User:", client.user['id'], "-", client.user['name'])
+
+
+print("[+] gathering sherlocks info")
+categories = defaultdict(list)
+sherlocks = client.get_all_sherlocks()
+sherlocks.sort(key=get_timestamp)
+total_completed = 0  # Variable to track total completed sherlocks
+
+print(f"[i] Retrieved {len(sherlocks)} sherlocks")
+
+# Group sherlocks by their categories
+for sherlock in sherlocks:
+    categories[sherlock['category_name']].append(sherlock)
+    if sherlock['progress'] == 100:
+        total_completed += 1  # Increment total completed if challenge is owned
+
+# Print out the grouped sherlocks with the number of completed and total sherlocks in each category
+for category, grouped_sherlocks in categories.items():
+    total = len(grouped_sherlocks)
+    completed = sum(1 for sherlock in grouped_sherlocks if sherlock['progress'] == 100)  # Count completed challenges
+    
+    res = ea.search_note(
+        search=f"note.title %= '{category}*'",
+        ancestorNoteId=trilium_sherlocks_folder,
+        ancestorDepth='eq1',
+        limit=1,
+        fastSearch=True,
+    )
+    
+    catId = ""
+    if res['results'] and res['results'][0]['title'].split(' - ')[0].strip().lower() == category.lower():
+	# page exists - lets check if the details have changed
+        ea.patch_note(noteId=res['results'][0]['noteId'], title=category+" - "+str(completed)+" / "+str(total))
+        catId = res['results'][0]['noteId']
+        print(f"[i] updated category: {category} - ({completed}/{total})")
+    else:
+        new_note = ea.create_note(
+            parentNoteId=trilium_sherlocks_folder,
+            type="text",
+            title=category+" - "+str(completed)+" / "+str(total),
+            content=" ",
+        )
+        catId = new_note['note']['noteId']
+        print(f"[+] created category: {catId} {category} - ({completed}/{total})")
+    
+    for sherlock in grouped_sherlocks:
+        #print(f" - ID: {challenge['id']}, Name: {challenge['name']}, Difficulty: {challenge['difficulty']}")
+        res2 = ea.search_note(
+            search=f"{sherlock['name']}",
+            ancestorNoteId=catId,
+            ancestorDepth='eq1',
+            limit=1,
+            fastSearch=True,
+        )
+        # already exists update the values
+        if res2['results'] and res2['results'][0]['title'].lower() == sherlock['name'].lower():
+            print(f"[i] found ID: {sherlock['id']}, Name: {sherlock['name']}, Difficulty: {sherlock['difficulty']}")
+    
+            for attribute in res2['results'][0]['attributes']:
+                if attribute['name'] == "Difficulty":
+                    ea.patch_attribute(attributeId=attribute['attributeId'], value=sherlock['difficulty'])
+        
+                if attribute['name'] == "Released":
+                    release_str = sherlock['release_date']
+                    release_date = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ")
+                    formatted_release_date = release_date.strftime("%d %B %Y")
+                    ea.patch_attribute(attributeId=attribute['attributeId'], value=formatted_release_date)
+        
+                if attribute['name'] == "Solved":
+                    ea.patch_attribute(attributeId=attribute['attributeId'], value=str(sherlock['progress']))
+        
+                if attribute['name'] == "cssClass":
+                    # Determine the cssClass value based on the progress
+                    if sherlock['progress'] == 0:
+                        ea.patch_attribute(attributeId=attribute['attributeId'], value="todo")
+                    elif sherlock['progress'] == 100:
+                        ea.patch_attribute(attributeId=attribute['attributeId'], value="done")
+                    elif 0 < sherlock['progress'] < 100:
+                        ea.patch_attribute(attributeId=attribute['attributeId'], value="inprogress")
+
+
+        else: # doesnt already exist, create page
+            release_str = sherlock['release_date']
+            release_date = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ")
+            formatted_release_date = release_date.strftime("%d %B %Y")
+            new_note = ea.create_note(
+                parentNoteId=catId,
+                type="text",
+                title=sherlock['name'],
+                content=" ",
+            )
+            ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="relation", name="template", value=trilium_sherlocks_template_id)
+            ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Difficulty", value=sherlock['difficulty'])
+            ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Released", value=formatted_release_date)
+            ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Solved", value=str(sherlock['progress']))
+            if sherlock['progress'] == 0:
+                ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="todo")
+            elif sherlock['progress'] == 100:
+                ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="done")
+            elif 0 < sherlock['progress'] < 100:
+                ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="inprogress")
+
+            print(f"[+] created ID: {sherlock['id']}, Name: {sherlock['name']}, Difficulty: {sherlock['difficulty']}")
+
+print("[+] updating folder name                                          ")
+ea.patch_note(noteId=trilium_sherlocks_folder,title="Sherlocks - "+str(total_completed)+" / "+str(len(sherlocks)))
diff --git a/htb2trilium/htb_client.py b/htb2trilium/htb_client.py
new file mode 100644
index 0000000..a35929a
--- /dev/null
+++ b/htb2trilium/htb_client.py
@@ -0,0 +1,214 @@
+import requests
+import warnings
+
+warnings.filterwarnings("ignore")
+
+class HTBClient:
+    def __init__(self, password):
+        self.password = password
+        self.base_url = 'https://labs.hackthebox.com/api/v4'
+        self.proxies = {
+            #"http": "http://127.0.0.1:8080",  # Burp proxy for HTTP traffic
+            #"https": "http://127.0.0.1:8080"  # Burp proxy for HTTPS traffic
+        }
+        # Fetch user info
+        self.user = self.get_user_info()
+        
+        # Fetch user stats and store them in self.user
+        user_owns, root_owns, respects = self.get_user_stats(self.user['id'])
+        self.user['user_owns'] = user_owns
+        self.user['root_owns'] = root_owns
+        self.user['respects'] = respects
+
+    def get_user_info(self):
+        headers = {
+            "Authorization": f"Bearer {self.password}",
+            "User-Agent": None  # Explicitly remove User-Agent
+        }
+        response = requests.get(
+            f'{self.base_url}/user/info', 
+            headers=headers, 
+            proxies=self.proxies,
+            verify=False  # Disable SSL verification
+        )
+        if response.status_code != 200:
+            raise Exception(f"Error fetching {self.base_url}/user/info user info: {response.status_code}, {response.text}")
+        
+        # Return the user info as a dictionary
+        data = response.json().get('info')
+        return {'id': data['id'], 'name': data['name'], 'email': data['email']}
+
+    def get_user_stats(self, user_id):
+        headers = {
+            "Authorization": f"Bearer {self.password}",
+            "User-Agent": None  # Explicitly remove User-Agent
+        }
+        response = requests.get(
+            f'{self.base_url}/user/profile/basic/{user_id}', 
+            headers=headers, 
+            proxies=self.proxies,
+            verify=False  # Disable SSL verification
+        )
+        if response.status_code != 200:
+            raise Exception(f"Error fetching user stats: {response.status_code}, {response.text}")
+        
+        # Extract user statistics from the response
+        data = response.json().get('profile')
+        user_owns = data['user_owns']
+        root_owns = data['system_owns']
+        respects = data['respects']
+        return user_owns, root_owns, respects
+
+    def get_active_machines(self):
+        machines = []
+        seen_ids = set()  # Track unique machine IDs
+        seen_names = set()  # Track unique machine names
+        page = 1
+
+        while True:
+            response = requests.get(
+                f'{self.base_url}/machine/paginated?per_page=100&page={page}',
+                headers={
+                    "Authorization": f"Bearer {self.password}",
+                    "User-Agent": None  # Explicitly remove User-Agent
+                },
+                proxies=self.proxies,
+                verify=False  # Disable SSL verification
+            )
+
+            if response.status_code != 200:
+                raise Exception(f"Error fetching active machines: {response.status_code}, {response.text}")
+
+            data = response.json()
+            for machine in data['data']:
+                if machine['id'] not in seen_ids and machine['name'] not in seen_names:
+                    machines.append(machine)
+                    seen_ids.add(machine['id'])
+                    seen_names.add(machine['name'])
+
+            # Check for pagination
+            if page >= data['meta']['last_page']:
+                break
+            page += 1
+        
+        return machines
+
+    def get_retired_machines(self):
+        machines = []
+        seen_ids = set()  # Track unique machine IDs
+        seen_names = set()  # Track unique machine names
+        page = 1
+
+        while True:
+            response = requests.get(
+                f'{self.base_url}/machine/list/retired/paginated?per_page=100&page={page}',
+                headers={
+                    "Authorization": f"Bearer {self.password}",
+                    "User-Agent": None  # Explicitly remove User-Agent
+                },
+                proxies=self.proxies,
+                verify=False  # Disable SSL verification
+            )
+
+            if response.status_code != 200:
+                raise Exception(f"Error fetching retired machines: {response.status_code}, {response.text}")
+
+            data = response.json()
+            for machine in data['data']:
+                if machine['id'] not in seen_ids and machine['name'] not in seen_names:
+                    machines.append(machine)
+                    seen_ids.add(machine['id'])
+                    seen_names.add(machine['name'])
+
+            # Check for pagination
+            if page >= data['meta']['last_page']:
+                break
+            page += 1
+        
+        return machines
+
+    def get_all_machines(self):
+        # Combine active and retired machines, ensuring no duplicates
+        active_machines = self.get_active_machines()
+        retired_machines = self.get_retired_machines()
+
+        all_machines = active_machines + retired_machines
+        seen_ids = set()  # Track unique machine IDs
+        seen_names = set()  # Track unique machine names
+        unique_machines = []
+
+        for machine in all_machines:
+            if machine['id'] not in seen_ids and machine['name'] not in seen_names:
+                unique_machines.append(machine)
+                seen_ids.add(machine['id'])
+                seen_names.add(machine['name'])
+
+        return unique_machines
+
+    def get_all_challenges(self):
+        challenges = []
+        seen_ids = set()  # Track unique machine IDs
+        seen_names = set()  # Track unique machine names
+        page = 1
+
+        while True:
+            response = requests.get(
+                f'{self.base_url}/challenges?per_page=100&page={page}',
+                headers={
+                    "Authorization": f"Bearer {self.password}",
+                    "User-Agent": None  # Explicitly remove User-Agent
+                },
+                proxies=self.proxies,
+                verify=False  # Disable SSL verification
+            )
+
+            if response.status_code != 200:
+                raise Exception(f"Error fetching challenges: {response.status_code}, {response.text}")
+
+            data = response.json()
+            for challenge in data['data']:
+                if challenge['id'] not in seen_ids and challenge['name'] not in seen_names:
+                    challenges.append(challenge)
+                    seen_ids.add(challenge['id'])
+                    seen_names.add(challenge['name'])
+
+            # Check for pagination
+            if page >= data['meta']['last_page']:
+                break
+            page += 1
+        
+        return challenges
+
+    def get_all_sherlocks(self):
+        sherlocks = []
+        seen_ids = set()  # Track unique machine IDs
+        seen_names = set()  # Track unique machine names
+        page = 1
+
+        while True:
+            response = requests.get(
+                f'{self.base_url}/sherlocks?per_page=100&page={page}',
+                headers={
+                    "Authorization": f"Bearer {self.password}",
+                    "User-Agent": None  # Explicitly remove User-Agent
+                },
+                proxies=self.proxies,
+                verify=False  # Disable SSL verification
+            )
+
+            if response.status_code != 200:
+                raise Exception(f"Error fetching sherlocks: {response.status_code}, {response.text}")
+
+            data = response.json()
+            for sherlock in data['data']:
+                if sherlock['id'] not in seen_ids and sherlock['name'] not in seen_names:
+                    sherlocks.append(sherlock)
+                    seen_ids.add(sherlock['id'])
+                    seen_names.add(sherlock['name'])
+
+            # Check for pagination
+            if page >= data['meta']['last_page']:
+                break
+            page += 1
+        
+        return sherlocks
\ No newline at end of file
diff --git a/htb2trilium/images/machine_view.png b/htb2trilium/images/machine_view.png
new file mode 100755
index 0000000..5bcb448
--- /dev/null
+++ b/htb2trilium/images/machine_view.png
Binary files differ
diff --git a/htb2trilium/images/machines_overview.png b/htb2trilium/images/machines_overview.png
new file mode 100755
index 0000000..2d84760
--- /dev/null
+++ b/htb2trilium/images/machines_overview.png
Binary files differ

diff --git a/README.md b/README.md
index f623b41..fefccb6 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,58 @@
 TriliumScripts
 ===============
 
-My collection of scripts for use with trilium
\ No newline at end of file
+Collection of scripts for use with trilium
+
+## syntax highlighting
+
+**highlight_moodlog.js** & **highlight_todo.js** - Both of these will apply appropriate syntaxy highlighting to the specific note.
+
+### installation
+
+1) add the code as the notes contents.
+
+2) set "note type" to "JS frontend"
+
+3) add an "owned attribute" of "#widget"
+
+### use
+
+on the note you want highlighting set the "owned attributes" to "#todotxt #hideHighlightWidget" or "#moodlog #hideHighlightWidget" depending on which syntax type you want to highlight
+
+## bidirectional sync
+
+**trilium_sync.php** - script to keep a folder of local text files and a trilium note in sync.
+
+### installation
+
+Edit the config values at the top of the script to match your trilium setup. Run it manually to test it is working as expected. If it is create a cron job.
+
+### example use
+
+```
+$> php trilium_sync.php 
+[i] checking connections
+[i] version: x.xx.x
+[i] gathering info
+
+- Folders -
+Folder/Path              Folder Name              Parent       noteId       Mod Trl    Mod Dsk    Status         
+Synced/                  Synced                   root         j0DRZhIzV97W 1742502936 1742507367 Exists
+Synced/BothPlaces/       BothPlaces               j0DRZhIzV97W AQb3Dg0yZHSu 1742507075 1742507200 Exists
+Synced/[del] tobe del/   [del] tobe del           j0DRZhIzV97W j0xUs1FdZL1Y 1742507321            Missing_Disk   
+Synced/AddToTrilium/     AddToTrilium                                                  1742507383 Missing_Trilium
+Synced/tobe deleted/     tobe deleted                                                  1742507200 Missing_Trilium
+- Files -
+Folder/Path              Filename                 Parent       noteId       Mod Trl    Mod Dsk    Hash  Status         
+Synced/BothPlaces/       UpdateDiskFile           AQb3Dg0yZHSu IwUZLwWdbiWG 1742507314 1742507199 Diff  Update_Disk    
+Synced/BothPlaces/       UpdateTriliumFile        AQb3Dg0yZHSu 7pMPgsb9AVRf 1742507130 1742507345 Diff  Update_Trilium 
+Synced/[del] tobe del/   gone_also.txt            j0xUs1FdZL1Y 4wOoHDy07csr 1742507178            N/A   Missing_Disk   
+Synced/AddToTrilium/     fromDiskToTrilium.txt                                         1742507394 N/A   Missing_Trilium
+Synced/tobe deleted/     gone_also.txt                                                 1742507200 N/A   Missing_Trilium
+
+[-] deleting disk folder: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/tobe deleted/
+[+] creating trilium folder: Synced/AddToTrilium/
+[+] creating trilium note: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/AddToTrilium/fromDiskToTrilium.txt
+[+] updating file on disk: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/BothPlaces/UpdateDiskFile
+[+] updating file on trilium: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/BothPlaces/UpdateTriliumFile
+```
\ No newline at end of file
diff --git a/highlight_moodlog.js b/highlight_moodlog.js
new file mode 100644
index 0000000..be4c283
--- /dev/null
+++ b/highlight_moodlog.js
@@ -0,0 +1,165 @@
+const TPL = `<div style="padding: 10px; border-top: 1px solid var(--main-border-color); contain: none;">
+    <strong>Moodlog Highlighting Applied</strong>
+</div>`;
+
+class MoodlogHighlighterWidget extends api.NoteContextAwareWidget {
+    static get parentWidget() { 
+        //console.log("MDLG: Getting parent widget: center-pane");
+        return 'center-pane'; 
+    }
+
+    get position() { 
+        //console.log("MDLG: Setting position: 100");
+        return 100; 
+    }
+
+    isEnabled() {
+        const enabled = super.isEnabled() 
+            && this.note.type === 'text' 
+            && this.note.hasLabel('moodlog');
+        //console.log(`MDLG: isEnabled: ${enabled}`);
+        return enabled;
+    }
+
+    doRender() {
+        //console.log("MDLG: Rendering widget...");
+        this.$widget = $(TPL);
+        //console.log("MDLG: Widget rendered:", this.$widget);
+        return this.$widget;
+    }
+
+    async refreshWithNote(note) {
+        //console.log("MDLG: Refreshing with note:", note);
+        const { content } = await note.getNoteComplement();
+        //console.log("MDLG: Note content fetched:", content);
+
+        // Check if content is fetched properly
+        if (!content) {
+            //console.log("MDLG: No content fetched. Exiting refresh.");
+            return;
+        }
+
+        const highlightedContent = this.applyHighlighting(content);
+        //console.log("MDLG: Highlighted content:", highlightedContent);
+
+        if (highlightedContent !== content) {
+            //console.log("MDLG: Updating note content with highlighted version");
+
+            // Use the setNoteContent function to update the note content
+            try {
+                await setNoteContent(note.noteId, highlightedContent);
+                //console.log("MDLG: Content updated with highlighted version");
+            } catch (error) {
+                //console.error("MDLG: Error updating content:", error);
+            }
+        } else {
+            //console.log("MDLG: No changes to content, skipping update.");
+        }
+    }
+
+
+ applyHighlighting(content) {
+    //console.log("MDLG: Applying highlighting to content...");
+
+    // Replace <br> with \n, then remove all other HTML tags
+    const plainTextContent = content.replace(/<br\s*\/?>/gi, '\n').replace(/<[^>]*>/g, ' ');
+    //console.log("MDLG: Stripped content:", plainTextContent);
+
+    // Combined regex for date & time
+    const dateTimePattern = /([1-2]\d{3}[-/.](0?[1-9]|1[0-2])[-/.](0[1-9]|[12]\d|3[01]))\s+(\d{1,2}:\d{2})/g;
+    const projectPattern = /\[\S+?\]/g;
+    const contextPattern = /(\s|^)(@\S+)/g;
+    const hashtagPattern = /(\s|^)(#\S+)/g;
+    const positivePattern = /(\s|^)(\+\S+)/g;
+    const negativePattern = /(\s|^)(-\S+)/g;
+
+    // Escape HTML characters to prevent issues with replacements
+    function escapeHtml(str) {
+        return str.replace(/[&<>"']/g, function (char) {
+            return {
+                '&': '&amp;',
+                '<': '&lt;',
+                '>': '&gt;',
+                '"': '&quot;',
+                "'": '&#039;'
+            }[char];
+        });
+    }
+
+      // Helper function to map the number to a colour based on the gradient
+    function getProjectColour(number) {
+        const minColor = { r: 107, g: 109, b: 229 };  // #6b6de5
+        const maxColor = { r: 70, g: 226, b: 96 };   // #46e260
+
+        const ratio = number / 9; // since we are working with 0-9
+
+        // Interpolate the RGB values between minColor and maxColor
+        const r = Math.round(minColor.r + ratio * (maxColor.r - minColor.r));
+        const g = Math.round(minColor.g + ratio * (maxColor.g - minColor.g));
+        const b = Math.round(minColor.b + ratio * (maxColor.b - minColor.b));
+
+        return `rgb(${r}, ${g}, ${b})`;
+    }
+     
+    // Process each line of the plain text content
+    let highlightedLines = plainTextContent.split("\n").map(line => {
+        //console.log("MDLG: Processing line:", line);
+
+        // Apply combined date & time highlight
+        line = line.replace(dateTimePattern, `<span style="color: rgb(188, 55, 237);">$1</span> <span style="color: rgb(188, 55, 237);">$4</span>`);
+
+        // Apply other highlights
+        line = line.replace(projectPattern, (match) => {
+            // Extract the number inside square brackets
+            const number = parseInt(match.match(/\d+/)[0], 10);
+            const colour = getProjectColour(number);
+            return `<span style="color: ${colour}; font-weight: bold;">${escapeHtml(match)}</span>`;
+        });
+        
+        // Apply other highlights
+        //line = line.replace(projectPattern, `<span style="color: rgb(224, 218, 36); font-weight: bold;">$&</span>`);
+        line = line.replace(contextPattern, (_, space, tag) => `${space}<span style="color: rgb(255 165 10);">${escapeHtml(tag)}</span>`);
+        line = line.replace(hashtagPattern, (_, space, tag) => `${space}<span style="color: rgb(41, 229, 242);">${escapeHtml(tag)}</span>`);
+        line = line.replace(positivePattern, (_, space, tag) => `${space}<span style="color: rgb(44, 227, 34);">${escapeHtml(tag)}</span>`);
+        line = line.replace(negativePattern, (_, space, tag) => `${space}<span style="color: rgb(230, 59, 44);">${escapeHtml(tag)}</span>`);
+
+        return line.trim();
+    });
+
+    // Join lines with proper <br> handling
+    let finalContent = highlightedLines.join("<br>");
+
+    //console.log("MDLG: Highlighted content:", finalContent);
+    return finalContent;
+}
+
+
+
+
+    async entitiesReloadedEvent({ loadResults }) {
+        //console.log("MDLG: Entities reloaded event triggered");
+        if (loadResults.isNoteContentReloaded(this.noteId)) {
+            //console.log("MDLG: Note content reloaded, refreshing widget...");
+            //this.refresh();
+        } else {
+            //console.log("MDLG: No content reload detected.");
+        }
+    }
+    
+
+}
+
+console.log("MDLG: Starting Moodlog Highlighter Widget");
+module.exports = MoodlogHighlighterWidget;
+
+function setNoteContent(noteId, content) {
+    /*const noteElement = document.querySelector(`note-detail-readonly-text-content`);
+
+    if (noteElement) {
+        noteElement.innerHTML = content; // Update the displayed content
+        console.log(`MDLG: Updated note ${noteId} in the DOM.`);
+    } else {
+        console.warn(`MDLG: Note element with data-note-id='${noteId}' not found.`);
+    }*/
+    return api.runOnBackend((id, data) => api.getNote(id).setContent(data), [noteId, content]);
+}
diff --git a/highlight_todotxt.js b/highlight_todotxt.js
new file mode 100644
index 0000000..1f1b007
--- /dev/null
+++ b/highlight_todotxt.js
@@ -0,0 +1,157 @@
+const TPL = `<div style="padding: 10px; border-top: 1px solid var(--main-border-color); contain: none;">
+    <strong>Todo Highlighting Applied</strong>
+</div>`;
+
+class MoodlogHighlighterWidget extends api.NoteContextAwareWidget {
+    static get parentWidget() { 
+        //console.log("MDLG: Getting parent widget: center-pane");
+        return 'center-pane'; 
+    }
+
+    get position() { 
+        //console.log("MDLG: Setting position: 100");
+        return 100; 
+    }
+
+    isEnabled() {
+        const enabled = super.isEnabled() 
+            && this.note.type === 'text' 
+            && this.note.hasLabel('todotxt');
+        //console.log(`MDLG: isEnabled: ${enabled}`);
+        return enabled;
+    }
+
+    doRender() {
+        //console.log("MDLG: Rendering widget...");
+        this.$widget = $(TPL);
+        //console.log("MDLG: Widget rendered:", this.$widget);
+        return this.$widget;
+    }
+
+    async refreshWithNote(note) {
+        //console.log("MDLG: Refreshing with note:", note);
+        const { content } = await note.getNoteComplement();
+        //console.log("MDLG: Note content fetched:", content);
+
+        // Check if content is fetched properly
+        if (!content) {
+            //console.log("MDLG: No content fetched. Exiting refresh.");
+            return;
+        }
+
+        const highlightedContent = this.applyHighlighting(content);
+        //console.log("MDLG: Highlighted content:", highlightedContent);
+
+        if (highlightedContent !== content) {
+            //console.log("MDLG: Updating note content with highlighted version");
+
+            // Use the setNoteContent function to update the note content
+            try {
+                await setNoteContent(note.noteId, highlightedContent);
+                //console.log("MDLG: Content updated with highlighted version");
+            } catch (error) {
+                //console.error("MDLG: Error updating content:", error);
+            }
+        } else {
+            //console.log("MDLG: No changes to content, skipping update.");
+        }
+    }
+
+
+applyHighlighting(content) {
+    // Replace <br> with \n, then remove all other HTML tags
+    const plainTextContent = content.replace(/<br\s*\/?\>/gi, '\n').replace(/<[^>]*>/g, ' ');
+
+    // Regular expressions for Todo.txt syntax
+    const datePattern = /\b(0[1-9]|[12]\d|3[01])\/(0[1-9]|1[0-2])\/([1-2]\d{3})\b/g;
+    const completedTaskPattern = /^\s*x\s+.*/g;
+    const priorityPattern = /^(\s*\([A-E]\))\s+/;
+    const projectPattern = /(?:\s|^)(\+\S+)/g;
+    const contextPattern = /(?:\s|^)(@\S+)/g;
+    const waitPattern = /\bWAIT\b/gi;
+    const customAttributePattern = /(\s+[^\s:]+:[^\s:]+)+\s*$/g;
+    
+    function escapeHtml(str) {
+        return str.replace(/[&<>"']/g, char => ({
+            '&': '&amp;', '<': '&lt;', '>': '&gt;',
+            '"': '&quot;', "'": '&#039;'
+        })[char]);
+    }
+
+    let highlightedLines = plainTextContent.split("\n").map(line => {
+        // If line starts with 'x', make the whole line grey
+        if (/^\s*x\s+/.test(line)) {
+            return `<span style="color: grey;">${line}</span>`;
+        }
+        
+        // Highlight priority with different colours based on level
+        line = line.replace(priorityPattern, (match, priority) => {
+            const level = priority.match(/[A-E]/)?.[0];
+            const colors = {
+                'A': 'rgb(255, 0, 0)',       // Red
+                'B': 'rgb(255, 102, 0)',     // Orange
+                'C': 'rgb(255, 204, 0)',     // Yellow
+                'D': 'rgb(0, 153, 255)',     // Blue
+                'E': 'rgb(102, 204, 102)'    // Green
+            };
+            const color = colors[level] || 'black';
+            return `<span style="color: ${color}; font-weight: bold;">${escapeHtml(priority)}</span> `;
+        });
+        
+        // Highlight date
+        line = line.replace(datePattern, (match) => {
+            return `<span style="color: rgb(188, 55, 237);">${escapeHtml(match)}</span>`;
+        });
+
+        // Highlight project tags
+        line = line.replace(projectPattern, (_, tag) => `<span style="color: rgb(224, 218, 36);"> ${escapeHtml(tag)}</span>`);
+        
+        // Highlight context tags
+        line = line.replace(contextPattern, (_, tag) => `<span style="color: rgb(255, 165, 10);"> ${escapeHtml(tag)}</span>`);
+        
+        // Highlight WAIT command
+        line = line.replace(waitPattern, `<span style="color: rgb(255, 69, 0);">WAIT</span>`);
+        
+        // Highlight custom attributes
+        line = line.replace(customAttributePattern, `<span style="color: rgb(100, 149, 237);">$1</span>`);
+        
+        // Remove leading space (if any) from the beginning of the line, without affecting spaces before + or @
+        line = line.replace(/^\s+/, '');
+        
+        return line.trim();
+    });
+
+    return highlightedLines.join("<br>");
+}
+
+
+
+
+
+    async entitiesReloadedEvent({ loadResults }) {
+        //console.log("MDLG: Entities reloaded event triggered");
+        if (loadResults.isNoteContentReloaded(this.noteId)) {
+            //console.log("MDLG: Note content reloaded, refreshing widget...");
+            //this.refresh();
+        } else {
+            //console.log("MDLG: No content reload detected.");
+        }
+    }
+    
+
+}
+
+console.log("TodoHighlight: Starting todotxt Highlighter Widget");
+module.exports = MoodlogHighlighterWidget;
+
+function setNoteContent(noteId, content) {
+    /*const noteElement = document.querySelector(`note-detail-readonly-text-content`);
+
+    if (noteElement) {
+        noteElement.innerHTML = content; // Update the displayed content
+        console.log(`MDLG: Updated note ${noteId} in the DOM.`);
+    } else {
+        console.warn(`MDLG: Note element with data-note-id='${noteId}' not found.`);
+    }*/
+    return api.runOnBackend((id, data) => api.getNote(id).setContent(data), [noteId, content]);
+}
diff --git a/htb2trilium/INSTALL.md b/htb2trilium/INSTALL.md
new file mode 100644
index 0000000..da8afad
--- /dev/null
+++ b/htb2trilium/INSTALL.md
@@ -0,0 +1,41 @@
+## scripts require:
+
+```
+ > pip3 install trilium-py
+```
+
+## config.json
+- get trilium token in trilium options->ETAPI and "create new token"
+- create a new note with a title "Machines"
+- click on this note -> "note info". Note ID is there. This ID goes in "trilium_machines_htb_folder" in the config file
+- set the folder's owned attributes to:
+
+```
+#label:user=promoted,single,text #label:root=promoted,single,text #label:respect=promoted,single,text #user=0 #root=0 #respect=0 
+```
+
+ - create a note named "HTBMachineTemplate" 
+ - set it's "owned attributes" to:
+
+```
+#template #label:User=promoted,single,text #label:Root=promoted,single,text #label:Tags=promoted,single,text
+```
+
+- get this page's ID and put in "trilium_machines_template_id"
+- create "challenges" page, this page's ID goes in: trilium_challenges_folder  
+- create "Sherlocks" page, this page's ID goes in: trilium_sherlocks_folder  
+- create 2 pages "HTBChallengesTemplate" and "HTBSherlocksTemplate", both of these should have the owned attributes:
+
+```
+#template #label:Difficulty=promoted,single,text #label:Solved=promoted,single,text #label:Released=promoted,single,text 
+```
+
+- The ID's of "HTBChallengesTemplate" and "HTBSherlocksTemplate" go in the matching config values.
+
+- to get "in progress" todo colour - add to trilium demo/scripting/taskmanager/implementation/CSS:
+
+```
+ span.fancytree-node.inprogress .fancytree-title {
+    color: orange !important;
+ }
+```
\ No newline at end of file
diff --git a/htb2trilium/README.md b/htb2trilium/README.md
new file mode 100644
index 0000000..1b45ae2
--- /dev/null
+++ b/htb2trilium/README.md
@@ -0,0 +1,16 @@
+htb2trilium
+===============
+
+Pull hackthebox info and stats into personal trilium note taking software
+
+single machine view:
+
+![machine view](images/machine_view.png)
+
+overview of machines:
+
+![machines overview](images/machines_overview.png)
+
+navigation:
+
+![navigation](images/navigation.png)
\ No newline at end of file
diff --git a/htb2trilium/config.json b/htb2trilium/config.json
new file mode 100644
index 0000000..39597ce
--- /dev/null
+++ b/htb2trilium/config.json
@@ -0,0 +1,11 @@
+{
+	"htb_code": "in https://app.hackthebox.com/profile/settings click 'profile settings' and under 'app tokens' you can 'create app token' - that goes here",
+	"trilium_server_url": "https://place.to.your.trilium.server",
+	"trilium_token": "",
+	"trilium_machines_htb_folder": "",
+	"trilium_machines_template_id": "",
+	"trilium_challenges_folder": "",
+	"trilium_challenges_template_id": "",
+	"trilium_sherlocks_folder": "",
+	"trilium_sherlocks_template_id": ""
+}
\ No newline at end of file
diff --git a/htb2trilium/htb2trilium_challenges.py b/htb2trilium/htb2trilium_challenges.py
new file mode 100644
index 0000000..d0d95ca
--- /dev/null
+++ b/htb2trilium/htb2trilium_challenges.py
@@ -0,0 +1,146 @@
+import os
+import json
+import requests
+from collections import defaultdict
+from bs4 import BeautifulSoup
+from datetime import datetime, timezone
+from htb_client import HTBClient
+from trilium_py.client import ETAPI
+
+# Get the absolute path of the script's directory
+script_dir = os.path.dirname(os.path.abspath(__file__))
+
+# Construct the full path to the config.json file
+config_path = os.path.join(script_dir, 'config.json')
+
+# Load configuration from the JSON file
+with open(config_path, 'r') as f:
+    config = json.load(f)
+
+# Accessing config values
+htb_code = config['htb_code']
+trilium_server_url = config['trilium_server_url']
+trilium_token = config['trilium_token']
+trilium_challenges_folder = config['trilium_challenges_folder']
+trilium_challenges_template_id = config['trilium_challenges_template_id']
+
+def get_timestamp(machine):
+    # Parse the release date string into a datetime object
+    release_str = machine['release_date']
+    dt = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ")
+    
+    # Set the timezone to UTC
+    dt = dt.replace(tzinfo=timezone.utc)
+    
+    # Get the timestamp
+    timestamp = dt.timestamp()
+    return timestamp
+
+print("[+] connecting to HTB")
+client = HTBClient(password=htb_code)
+print("[+] connecting to trilium")
+ea = ETAPI(trilium_server_url, trilium_token)
+print("[i] version: ", ea.app_info()['appVersion'])
+
+print("[i] HTB User:", client.user['id'], "-", client.user['name'])
+
+
+print("[+] gathering challenges info")
+categories = defaultdict(list)
+challenges = client.get_all_challenges()
+challenges.sort(key=get_timestamp)
+total_completed = 0  # Variable to track total completed challenges
+
+print(f"[i] Retrieved {len(challenges)} challenges")
+
+# Group challenges by their categories
+for challenge in challenges:
+    categories[challenge['category_name']].append(challenge)
+    if challenge['is_owned']:
+        total_completed += 1  # Increment total completed if challenge is owned
+
+# Print out the grouped challenges with the number of completed and total challenges in each category
+for category, grouped_challenges in categories.items():
+    total = len(grouped_challenges)
+    completed = sum(1 for challenge in grouped_challenges if challenge['is_owned'])  # Count completed challenges
+    
+    res = ea.search_note(
+        search=f"note.title %= '{category}*'",
+        ancestorNoteId=trilium_challenges_folder,
+        ancestorDepth='eq1',
+        limit=1,
+        fastSearch=True,
+    )
+    
+    catId = ""
+    if res['results'] and res['results'][0]['title'].split(' - ')[0].strip().lower() == category.lower():
+        # page exists - lets check if the details have changed
+        ea.patch_note(noteId=res['results'][0]['noteId'], title=category+" - "+str(completed)+" / "+str(total))
+        catId = res['results'][0]['noteId']
+        print(f"[i] updated category: {category} - ({completed}/{total})")
+    else:
+        new_note = ea.create_note(
+            parentNoteId=trilium_challenges_folder,
+            type="text",
+            title=category+" - "+str(completed)+" / "+str(total),
+            content=" ",
+        )
+        catId = new_note['note']['noteId']
+        print(f"[+] created category: {catId} {category} - ({completed}/{total})")
+    
+    for challenge in grouped_challenges:
+        #print(f" - ID: {challenge['id']}, Name: {challenge['name']}, Difficulty: {challenge['difficulty']}")
+        res2 = ea.search_note(
+            search=f"{challenge['name']}",
+            ancestorNoteId=catId,
+            ancestorDepth='eq1',
+            limit=1,
+            fastSearch=True,
+        )
+        # already exists update the values
+        if res2['results'] and res2['results'][0]['title'].lower() == challenge['name'].lower():
+            print(f"[i] found ID: {challenge['id']}, Name: {challenge['name']}, Difficulty: {challenge['difficulty']}")
+            #print(f"Search response for challenge '{challenge['name']}': {res2}")
+            for attribute in res2['results'][0]['attributes']:
+                if attribute['name'] == "Difficulty":
+                    ea.patch_attribute(attributeId=attribute['attributeId'], value=challenge['difficulty'])
+                if attribute['name'] == "Released":
+                    release_str = challenge['release_date']
+                    release_date = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ")
+                    formatted_release_date = release_date.strftime("%d %B %Y")
+                    ea.patch_attribute(attributeId=attribute['attributeId'], value=formatted_release_date)
+                if attribute['name'] == "Solved":
+                    if challenge['is_owned']:
+                        ea.patch_attribute(attributeId=attribute['attributeId'], value="done")
+                    else:
+                        ea.patch_attribute(attributeId=attribute['attributeId'], value=" ")
+                if attribute['name'] == "cssClass":
+                    if challenge['is_owned']:
+                        ea.patch_attribute(attributeId=attribute['attributeId'], value="done")
+                    else:
+                        ea.patch_attribute(attributeId=attribute['attributeId'], value="todo")
+
+        else: # doesnt already exist, create page
+            release_str = challenge['release_date']
+            release_date = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ")
+            formatted_release_date = release_date.strftime("%d %B %Y")
+            new_note = ea.create_note(
+                parentNoteId=catId,
+                type="text",
+                title=challenge['name'],
+                content=" ",
+            )
+            ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="relation", name="template", value=trilium_challenges_template_id)
+            ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Difficulty", value=challenge['difficulty'])
+            ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Released", value=formatted_release_date)
+            if challenge['is_owned']:
+                ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="done")
+                ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Solved", value="done")
+            else:
+                ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="todo")
+                ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Solved", value=" ")
+
+            print(f"[+] created ID: {challenge['id']}, Name: {challenge['name']}, Difficulty: {challenge['difficulty']}")
+
+print("[+] updating folder name                                          ")
+ea.patch_note(noteId=trilium_challenges_folder,title="Challenges - "+str(total_completed)+" / "+str(len(challenges)))
diff --git a/htb2trilium/htb2trilium_machines.py b/htb2trilium/htb2trilium_machines.py
new file mode 100644
index 0000000..de8be53
--- /dev/null
+++ b/htb2trilium/htb2trilium_machines.py
@@ -0,0 +1,203 @@
+import os
+import json
+import requests
+from bs4 import BeautifulSoup
+from datetime import datetime, timezone
+from htb_client import HTBClient
+from trilium_py.client import ETAPI
+
+# Get the absolute path of the script's directory
+script_dir = os.path.dirname(os.path.abspath(__file__))
+
+# Construct the full path to the config.json file
+config_path = os.path.join(script_dir, 'config.json')
+
+# Load configuration from the JSON file
+with open(config_path, 'r') as f:
+    config = json.load(f)
+
+# Accessing config values
+htb_code = config['htb_code']
+trilium_server_url = config['trilium_server_url']
+trilium_token = config['trilium_token']
+trilium_machines_htb_folder = config['trilium_machines_htb_folder']
+trilium_machines_template_id = config['trilium_machines_template_id']
+
+def generate_newpage(machine):
+	no = "<span style=\"color:hsl(0,75%,60%);\">No</span>"
+	yes = "<span style=\"color:hsl(120,75%,60%);\">Yes</span>"
+	user_colour = no
+	if machine['authUserInUserOwns']:
+		user_colour = yes
+	root_colour = no
+	if machine['authUserInRootOwns']:
+		root_colour = yes
+
+	status = "Retired"
+	if machine['active']:
+		status = "Active"
+
+	release_str = machine['release']
+	release_date = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ")
+	formatted_release_date = release_date.strftime("%d %B %Y")
+
+	html = """
+	<figure class="image image-style-align-left image_resized" style="width:12.28%;">
+	  <img src="https://www.hackthebox.com{avatar}">
+	</figure>
+	<p>
+	  <strong>OS:</strong> {os}<br>
+	  <strong>Difficulty:</strong> {difficultyText} <br>
+	  <strong>Rating:</strong> {rating} / 5<br>
+	  <strong>Points:</strong> {points}<br>
+	  <strong>User / Root: </strong> {user_colour} / {root_colour}<br>
+	  <strong>Released:</strong> {release_date}<br>
+	  <strong>State:</strong> {status}
+	</p>
+	<hr>
+	<h2>Notes</h2>
+	<p>&nbsp;</p>
+	""".format(
+		os=machine['os'],
+		difficultyText=machine['difficultyText'],
+		rating=machine['star'],
+		points=machine['points'],
+		release_date=formatted_release_date,
+		user_colour=user_colour,
+		root_colour=root_colour,
+		status = status,
+		avatar = machine['avatar'],
+	)
+
+	return html
+
+def get_timestamp(machine):
+    # Parse the release date string into a datetime object
+    release_str = machine['release']
+    dt = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ")
+    
+    # Set the timezone to UTC
+    dt = dt.replace(tzinfo=timezone.utc)
+    
+    # Get the timestamp
+    timestamp = dt.timestamp()
+    return timestamp
+
+print("[+] connecting to HTB")
+client = HTBClient(password=htb_code)
+print("[+] connecting to trilium")
+ea = ETAPI(trilium_server_url, trilium_token)
+print("[i] version: ", ea.app_info()['appVersion'])
+
+print("[i] HTB User:", client.user['id'], "-", client.user['name'])
+print("[i] user owns:", client.user['user_owns'], "| Root owns:", client.user['root_owns'], "| Respect:", client.user['respects'])
+
+master_folder = ea.get_note(trilium_machines_htb_folder)
+for attribute in master_folder['attributes']:
+	if attribute['name'] == "user":
+		if attribute['value'] != str(client.user['user_owns']):
+			print("[+] updating user owns (folder attribute)")
+			ea.patch_attribute(attributeId=attribute['attributeId'], value=str(client.user['user_owns']))
+	if attribute['name'] == "root":
+		if attribute['value'] != str(client.user['root_owns']):
+			print("[+] updating root owns (folder attribute)")
+			ea.patch_attribute(attributeId=attribute['attributeId'], value=str(client.user['root_owns']))
+	if attribute['name'] == "respect":
+		if attribute['value'] != str(client.user['respects']):
+			print("[+] updating respect (folder attribute)")
+			ea.patch_attribute(attributeId=attribute['attributeId'], value=str(client.user['respects']))
+
+print("[+] gathering machines info")
+machines = client.get_all_machines()
+machines.sort(key=get_timestamp)
+
+print(f"[i] Retrieved {len(machines)} machines")
+#for machine in machines:
+#    print(f" - ID: {machine['id']}, Name: {machine['name']}, OS: {machine['os']}, Difficulty: {machine['difficultyText']}")
+
+machine_count = 0
+completed_count = 0
+for machine in machines:
+	machine_count += 1
+	print('processing: ',machine_count, "/", len(machines), "("+machine['name']+")               " , end='\r')
+
+	res = ea.search_note(
+		search="\""+machine['name']+"\"",
+		ancestorNoteId=trilium_machines_htb_folder,
+		ancestorDepth='eq1',
+		limit=1,
+		fastSearch=True,
+	)
+
+	if res['results'] and machine['name'] == res['results'][0]['title']:
+		# page exists - lets check if the details have changed
+
+		current_html = ea.get_note_content(noteId=res['results'][0]['noteId'])
+		current_soup = BeautifulSoup(current_html, 'html.parser')
+		current_paragraph = current_soup.find_all('p')[0].text
+
+		new_html = generate_newpage(machine)
+		new_soup = BeautifulSoup(new_html, 'html.parser')
+		new_paragraph = new_soup.find_all('p')[0].text
+
+		# current page contains first paragraph of "blank" (useful for when it doesnt create or find the note properly.. shouldnt get here)
+		if current_paragraph == "blank":
+			ea.update_note_content(noteId=res['results'][0]['noteId'], content=new_html)
+			ea.create_attribute(attributeId=None, isInheritable=False, noteId=res['results'][0]['noteId'], type="relation", name="template", value=trilium_machines_htb_folder)
+			if machine['authUserInUserOwns'] and machine['authUserInRootOwns']:
+				ea.create_attribute(attributeId=None, isInheritable=False, noteId=res['results'][0]['noteId'], type="label", name="cssClass", value="done")
+			else:
+				if machine['authUserInUserOwns'] or machine['authUserInRootOwns']:
+					ea.create_attribute(attributeId=None, isInheritable=False, noteId=res['results'][0]['noteId'], type="label", name="cssClass", value="inprogress")
+				else:
+					ea.create_attribute(attributeId=None, isInheritable=False, noteId=res['results'][0]['noteId'], type="label", name="cssClass", value="todo")
+			# re-get the current content
+			current_html = ea.get_note_content(noteId=res['results'][0]['noteId'])
+			current_soup = BeautifulSoup(current_html, 'html.parser')
+
+		if current_paragraph != new_paragraph:
+
+			# details have updated!
+			print("[+] updating page:",machine['name'], "-> "+res['results'][0]['title']+"                 ")
+			replacement = current_soup.find('p')
+			replacement.replace_with( new_soup.find_all('p')[0] )
+			ea.update_note_content(noteId=res['results'][0]['noteId'], content=current_soup)
+
+			# now to update the label
+			for attribute in res['results'][0]['attributes']:
+				if attribute['name'] == "cssClass":
+					if machine['authUserInUserOwns'] and machine['authUserInRootOwns']:
+						ea.patch_attribute(attributeId=attribute['attributeId'], value="done")
+					else:
+						if machine['authUserInUserOwns'] or machine['authUserInRootOwns']:
+							ea.patch_attribute(attributeId=attribute['attributeId'], value="inprogress")
+						else:
+							ea.patch_attribute(attributeId=attribute['attributeId'], value="todo")
+							
+	else:
+		# title does not exist - create the note
+		html = generate_newpage(machine)
+		new_note = ea.create_note(
+			parentNoteId=trilium_machines_htb_folder,
+			type="text",
+			title=machine['name'],
+			content=html,
+		)
+		print("[+] created note:", machine['name'], "                        ")
+
+		ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="relation", name="template", value=trilium_machines_htb_folder)
+		if machine['authUserInUserOwns'] and machine['authUserInRootOwns']:
+			ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="done")
+		else:
+			if machine['authUserInUserOwns'] or machine['authUserInRootOwns']:
+				ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="inprogress")
+			else:
+				ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="todo")
+
+	if machine['authUserInUserOwns'] and machine['authUserInRootOwns']:
+			completed_count += 1
+
+print("[+] updating folder name                                          ")
+ea.patch_note(noteId=trilium_machines_htb_folder,title="Machines - "+str(completed_count)+" / "+str(len(machines)))
+
+print("[=] processed", machine_count)
\ No newline at end of file
diff --git a/htb2trilium/htb2trilium_sherlocks.py b/htb2trilium/htb2trilium_sherlocks.py
new file mode 100644
index 0000000..1e32e59
--- /dev/null
+++ b/htb2trilium/htb2trilium_sherlocks.py
@@ -0,0 +1,151 @@
+import os
+import json
+import requests
+from collections import defaultdict
+from bs4 import BeautifulSoup
+from datetime import datetime, timezone
+from htb_client import HTBClient
+from trilium_py.client import ETAPI
+
+# Get the absolute path of the script's directory
+script_dir = os.path.dirname(os.path.abspath(__file__))
+
+# Construct the full path to the config.json file
+config_path = os.path.join(script_dir, 'config.json')
+
+# Load configuration from the JSON file
+with open(config_path, 'r') as f:
+    config = json.load(f)
+
+# Accessing config values
+htb_code = config['htb_code']
+trilium_server_url = config['trilium_server_url']
+trilium_token = config['trilium_token']
+trilium_sherlocks_folder = config['trilium_sherlocks_folder']
+trilium_sherlocks_template_id = config['trilium_sherlocks_template_id']
+
+def get_timestamp(machine):
+    # Parse the release date string into a datetime object
+    release_str = machine['release_date']
+    dt = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ")
+    
+    # Set the timezone to UTC
+    dt = dt.replace(tzinfo=timezone.utc)
+    
+    # Get the timestamp
+    timestamp = dt.timestamp()
+    return timestamp
+
+print("[+] connecting to HTB")
+client = HTBClient(password=htb_code)
+print("[+] connecting to trilium")
+ea = ETAPI(trilium_server_url, trilium_token)
+print("[i] version: ", ea.app_info()['appVersion'])
+
+print("[i] HTB User:", client.user['id'], "-", client.user['name'])
+
+
+print("[+] gathering sherlocks info")
+categories = defaultdict(list)
+sherlocks = client.get_all_sherlocks()
+sherlocks.sort(key=get_timestamp)
+total_completed = 0  # Variable to track total completed sherlocks
+
+print(f"[i] Retrieved {len(sherlocks)} sherlocks")
+
+# Group sherlocks by their categories
+for sherlock in sherlocks:
+    categories[sherlock['category_name']].append(sherlock)
+    if sherlock['progress'] == 100:
+        total_completed += 1  # Increment total completed if challenge is owned
+
+# Print out the grouped sherlocks with the number of completed and total sherlocks in each category
+for category, grouped_sherlocks in categories.items():
+    total = len(grouped_sherlocks)
+    completed = sum(1 for sherlock in grouped_sherlocks if sherlock['progress'] == 100)  # Count completed challenges
+    
+    res = ea.search_note(
+        search=f"note.title %= '{category}*'",
+        ancestorNoteId=trilium_sherlocks_folder,
+        ancestorDepth='eq1',
+        limit=1,
+        fastSearch=True,
+    )
+    
+    catId = ""
+    if res['results'] and res['results'][0]['title'].split(' - ')[0].strip().lower() == category.lower():
+	# page exists - lets check if the details have changed
+        ea.patch_note(noteId=res['results'][0]['noteId'], title=category+" - "+str(completed)+" / "+str(total))
+        catId = res['results'][0]['noteId']
+        print(f"[i] updated category: {category} - ({completed}/{total})")
+    else:
+        new_note = ea.create_note(
+            parentNoteId=trilium_sherlocks_folder,
+            type="text",
+            title=category+" - "+str(completed)+" / "+str(total),
+            content=" ",
+        )
+        catId = new_note['note']['noteId']
+        print(f"[+] created category: {catId} {category} - ({completed}/{total})")
+    
+    for sherlock in grouped_sherlocks:
+        #print(f" - ID: {challenge['id']}, Name: {challenge['name']}, Difficulty: {challenge['difficulty']}")
+        res2 = ea.search_note(
+            search=f"{sherlock['name']}",
+            ancestorNoteId=catId,
+            ancestorDepth='eq1',
+            limit=1,
+            fastSearch=True,
+        )
+        # already exists update the values
+        if res2['results'] and res2['results'][0]['title'].lower() == sherlock['name'].lower():
+            print(f"[i] found ID: {sherlock['id']}, Name: {sherlock['name']}, Difficulty: {sherlock['difficulty']}")
+    
+            for attribute in res2['results'][0]['attributes']:
+                if attribute['name'] == "Difficulty":
+                    ea.patch_attribute(attributeId=attribute['attributeId'], value=sherlock['difficulty'])
+        
+                if attribute['name'] == "Released":
+                    release_str = sherlock['release_date']
+                    release_date = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ")
+                    formatted_release_date = release_date.strftime("%d %B %Y")
+                    ea.patch_attribute(attributeId=attribute['attributeId'], value=formatted_release_date)
+        
+                if attribute['name'] == "Solved":
+                    ea.patch_attribute(attributeId=attribute['attributeId'], value=str(sherlock['progress']))
+        
+                if attribute['name'] == "cssClass":
+                    # Determine the cssClass value based on the progress
+                    if sherlock['progress'] == 0:
+                        ea.patch_attribute(attributeId=attribute['attributeId'], value="todo")
+                    elif sherlock['progress'] == 100:
+                        ea.patch_attribute(attributeId=attribute['attributeId'], value="done")
+                    elif 0 < sherlock['progress'] < 100:
+                        ea.patch_attribute(attributeId=attribute['attributeId'], value="inprogress")
+
+
+        else: # doesnt already exist, create page
+            release_str = sherlock['release_date']
+            release_date = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ")
+            formatted_release_date = release_date.strftime("%d %B %Y")
+            new_note = ea.create_note(
+                parentNoteId=catId,
+                type="text",
+                title=sherlock['name'],
+                content=" ",
+            )
+            ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="relation", name="template", value=trilium_sherlocks_template_id)
+            ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Difficulty", value=sherlock['difficulty'])
+            ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Released", value=formatted_release_date)
+            ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Solved", value=str(sherlock['progress']))
+            if sherlock['progress'] == 0:
+                ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="todo")
+            elif sherlock['progress'] == 100:
+                ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="done")
+            elif 0 < sherlock['progress'] < 100:
+                ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="inprogress")
+
+            print(f"[+] created ID: {sherlock['id']}, Name: {sherlock['name']}, Difficulty: {sherlock['difficulty']}")
+
+print("[+] updating folder name                                          ")
+ea.patch_note(noteId=trilium_sherlocks_folder,title="Sherlocks - "+str(total_completed)+" / "+str(len(sherlocks)))
diff --git a/htb2trilium/htb_client.py b/htb2trilium/htb_client.py
new file mode 100644
index 0000000..a35929a
--- /dev/null
+++ b/htb2trilium/htb_client.py
@@ -0,0 +1,214 @@
+import requests
+import warnings
+
+warnings.filterwarnings("ignore")
+
+class HTBClient:
+    def __init__(self, password):
+        self.password = password
+        self.base_url = 'https://labs.hackthebox.com/api/v4'
+        self.proxies = {
+            #"http": "http://127.0.0.1:8080",  # Burp proxy for HTTP traffic
+            #"https": "http://127.0.0.1:8080"  # Burp proxy for HTTPS traffic
+        }
+        # Fetch user info
+        self.user = self.get_user_info()
+        
+        # Fetch user stats and store them in self.user
+        user_owns, root_owns, respects = self.get_user_stats(self.user['id'])
+        self.user['user_owns'] = user_owns
+        self.user['root_owns'] = root_owns
+        self.user['respects'] = respects
+
+    def get_user_info(self):
+        headers = {
+            "Authorization": f"Bearer {self.password}",
+            "User-Agent": None  # Explicitly remove User-Agent
+        }
+        response = requests.get(
+            f'{self.base_url}/user/info', 
+            headers=headers, 
+            proxies=self.proxies,
+            verify=False  # Disable SSL verification
+        )
+        if response.status_code != 200:
+            raise Exception(f"Error fetching {self.base_url}/user/info user info: {response.status_code}, {response.text}")
+        
+        # Return the user info as a dictionary
+        data = response.json().get('info')
+        return {'id': data['id'], 'name': data['name'], 'email': data['email']}
+
+    def get_user_stats(self, user_id):
+        headers = {
+            "Authorization": f"Bearer {self.password}",
+            "User-Agent": None  # Explicitly remove User-Agent
+        }
+        response = requests.get(
+            f'{self.base_url}/user/profile/basic/{user_id}', 
+            headers=headers, 
+            proxies=self.proxies,
+            verify=False  # Disable SSL verification
+        )
+        if response.status_code != 200:
+            raise Exception(f"Error fetching user stats: {response.status_code}, {response.text}")
+        
+        # Extract user statistics from the response
+        data = response.json().get('profile')
+        user_owns = data['user_owns']
+        root_owns = data['system_owns']
+        respects = data['respects']
+        return user_owns, root_owns, respects
+
+    def get_active_machines(self):
+        machines = []
+        seen_ids = set()  # Track unique machine IDs
+        seen_names = set()  # Track unique machine names
+        page = 1
+
+        while True:
+            response = requests.get(
+                f'{self.base_url}/machine/paginated?per_page=100&page={page}',
+                headers={
+                    "Authorization": f"Bearer {self.password}",
+                    "User-Agent": None  # Explicitly remove User-Agent
+                },
+                proxies=self.proxies,
+                verify=False  # Disable SSL verification
+            )
+
+            if response.status_code != 200:
+                raise Exception(f"Error fetching active machines: {response.status_code}, {response.text}")
+
+            data = response.json()
+            for machine in data['data']:
+                if machine['id'] not in seen_ids and machine['name'] not in seen_names:
+                    machines.append(machine)
+                    seen_ids.add(machine['id'])
+                    seen_names.add(machine['name'])
+
+            # Check for pagination
+            if page >= data['meta']['last_page']:
+                break
+            page += 1
+        
+        return machines
+
+    def get_retired_machines(self):
+        machines = []
+        seen_ids = set()  # Track unique machine IDs
+        seen_names = set()  # Track unique machine names
+        page = 1
+
+        while True:
+            response = requests.get(
+                f'{self.base_url}/machine/list/retired/paginated?per_page=100&page={page}',
+                headers={
+                    "Authorization": f"Bearer {self.password}",
+                    "User-Agent": None  # Explicitly remove User-Agent
+                },
+                proxies=self.proxies,
+                verify=False  # Disable SSL verification
+            )
+
+            if response.status_code != 200:
+                raise Exception(f"Error fetching retired machines: {response.status_code}, {response.text}")
+
+            data = response.json()
+            for machine in data['data']:
+                if machine['id'] not in seen_ids and machine['name'] not in seen_names:
+                    machines.append(machine)
+                    seen_ids.add(machine['id'])
+                    seen_names.add(machine['name'])
+
+            # Check for pagination
+            if page >= data['meta']['last_page']:
+                break
+            page += 1
+        
+        return machines
+
+    def get_all_machines(self):
+        # Combine active and retired machines, ensuring no duplicates
+        active_machines = self.get_active_machines()
+        retired_machines = self.get_retired_machines()
+
+        all_machines = active_machines + retired_machines
+        seen_ids = set()  # Track unique machine IDs
+        seen_names = set()  # Track unique machine names
+        unique_machines = []
+
+        for machine in all_machines:
+            if machine['id'] not in seen_ids and machine['name'] not in seen_names:
+                unique_machines.append(machine)
+                seen_ids.add(machine['id'])
+                seen_names.add(machine['name'])
+
+        return unique_machines
+
+    def get_all_challenges(self):
+        challenges = []
+        seen_ids = set()  # Track unique machine IDs
+        seen_names = set()  # Track unique machine names
+        page = 1
+
+        while True:
+            response = requests.get(
+                f'{self.base_url}/challenges?per_page=100&page={page}',
+                headers={
+                    "Authorization": f"Bearer {self.password}",
+                    "User-Agent": None  # Explicitly remove User-Agent
+                },
+                proxies=self.proxies,
+                verify=False  # Disable SSL verification
+            )
+
+            if response.status_code != 200:
+                raise Exception(f"Error fetching challenges: {response.status_code}, {response.text}")
+
+            data = response.json()
+            for challenge in data['data']:
+                if challenge['id'] not in seen_ids and challenge['name'] not in seen_names:
+                    challenges.append(challenge)
+                    seen_ids.add(challenge['id'])
+                    seen_names.add(challenge['name'])
+
+            # Check for pagination
+            if page >= data['meta']['last_page']:
+                break
+            page += 1
+        
+        return challenges
+
+    def get_all_sherlocks(self):
+        sherlocks = []
+        seen_ids = set()  # Track unique machine IDs
+        seen_names = set()  # Track unique machine names
+        page = 1
+
+        while True:
+            response = requests.get(
+                f'{self.base_url}/sherlocks?per_page=100&page={page}',
+                headers={
+                    "Authorization": f"Bearer {self.password}",
+                    "User-Agent": None  # Explicitly remove User-Agent
+                },
+                proxies=self.proxies,
+                verify=False  # Disable SSL verification
+            )
+
+            if response.status_code != 200:
+                raise Exception(f"Error fetching sherlocks: {response.status_code}, {response.text}")
+
+            data = response.json()
+            for sherlock in data['data']:
+                if sherlock['id'] not in seen_ids and sherlock['name'] not in seen_names:
+                    sherlocks.append(sherlock)
+                    seen_ids.add(sherlock['id'])
+                    seen_names.add(sherlock['name'])
+
+            # Check for pagination
+            if page >= data['meta']['last_page']:
+                break
+            page += 1
+        
+        return sherlocks
\ No newline at end of file
diff --git a/htb2trilium/images/machine_view.png b/htb2trilium/images/machine_view.png
new file mode 100755
index 0000000..5bcb448
--- /dev/null
+++ b/htb2trilium/images/machine_view.png
Binary files differ
diff --git a/htb2trilium/images/machines_overview.png b/htb2trilium/images/machines_overview.png
new file mode 100755
index 0000000..2d84760
--- /dev/null
+++ b/htb2trilium/images/machines_overview.png
Binary files differ
diff --git a/htb2trilium/images/navigation.png b/htb2trilium/images/navigation.png
new file mode 100755
index 0000000..fda8624
--- /dev/null
+++ b/htb2trilium/images/navigation.png
Binary files differ

diff --git a/README.md b/README.md
index f623b41..fefccb6 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,58 @@
 TriliumScripts
 ===============
 
-My collection of scripts for use with trilium
\ No newline at end of file
+Collection of scripts for use with trilium
+
+## syntax highlighting
+
+**highlight_moodlog.js** & **highlight_todo.js** - Both of these will apply appropriate syntaxy highlighting to the specific note.
+
+### installation
+
+1) add the code as the notes contents.
+
+2) set "note type" to "JS frontend"
+
+3) add an "owned attribute" of "#widget"
+
+### use
+
+on the note you want highlighting set the "owned attributes" to "#todotxt #hideHighlightWidget" or "#moodlog #hideHighlightWidget" depending on which syntax type you want to highlight
+
+## bidirectional sync
+
+**trilium_sync.php** - script to keep a folder of local text files and a trilium note in sync.
+
+### installation
+
+Edit the config values at the top of the script to match your trilium setup. Run it manually to test it is working as expected. If it is create a cron job.
+
+### example use
+
+```
+$> php trilium_sync.php 
+[i] checking connections
+[i] version: x.xx.x
+[i] gathering info
+
+- Folders -
+Folder/Path              Folder Name              Parent       noteId       Mod Trl    Mod Dsk    Status         
+Synced/                  Synced                   root         j0DRZhIzV97W 1742502936 1742507367 Exists
+Synced/BothPlaces/       BothPlaces               j0DRZhIzV97W AQb3Dg0yZHSu 1742507075 1742507200 Exists
+Synced/[del] tobe del/   [del] tobe del           j0DRZhIzV97W j0xUs1FdZL1Y 1742507321            Missing_Disk   
+Synced/AddToTrilium/     AddToTrilium                                                  1742507383 Missing_Trilium
+Synced/tobe deleted/     tobe deleted                                                  1742507200 Missing_Trilium
+- Files -
+Folder/Path              Filename                 Parent       noteId       Mod Trl    Mod Dsk    Hash  Status         
+Synced/BothPlaces/       UpdateDiskFile           AQb3Dg0yZHSu IwUZLwWdbiWG 1742507314 1742507199 Diff  Update_Disk    
+Synced/BothPlaces/       UpdateTriliumFile        AQb3Dg0yZHSu 7pMPgsb9AVRf 1742507130 1742507345 Diff  Update_Trilium 
+Synced/[del] tobe del/   gone_also.txt            j0xUs1FdZL1Y 4wOoHDy07csr 1742507178            N/A   Missing_Disk   
+Synced/AddToTrilium/     fromDiskToTrilium.txt                                         1742507394 N/A   Missing_Trilium
+Synced/tobe deleted/     gone_also.txt                                                 1742507200 N/A   Missing_Trilium
+
+[-] deleting disk folder: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/tobe deleted/
+[+] creating trilium folder: Synced/AddToTrilium/
+[+] creating trilium note: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/AddToTrilium/fromDiskToTrilium.txt
+[+] updating file on disk: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/BothPlaces/UpdateDiskFile
+[+] updating file on trilium: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/BothPlaces/UpdateTriliumFile
+```
\ No newline at end of file
diff --git a/highlight_moodlog.js b/highlight_moodlog.js
new file mode 100644
index 0000000..be4c283
--- /dev/null
+++ b/highlight_moodlog.js
@@ -0,0 +1,165 @@
+const TPL = `<div style="padding: 10px; border-top: 1px solid var(--main-border-color); contain: none;">
+    <strong>Moodlog Highlighting Applied</strong>
+</div>`;
+
+class MoodlogHighlighterWidget extends api.NoteContextAwareWidget {
+    static get parentWidget() { 
+        //console.log("MDLG: Getting parent widget: center-pane");
+        return 'center-pane'; 
+    }
+
+    get position() { 
+        //console.log("MDLG: Setting position: 100");
+        return 100; 
+    }
+
+    isEnabled() {
+        const enabled = super.isEnabled() 
+            && this.note.type === 'text' 
+            && this.note.hasLabel('moodlog');
+        //console.log(`MDLG: isEnabled: ${enabled}`);
+        return enabled;
+    }
+
+    doRender() {
+        //console.log("MDLG: Rendering widget...");
+        this.$widget = $(TPL);
+        //console.log("MDLG: Widget rendered:", this.$widget);
+        return this.$widget;
+    }
+
+    async refreshWithNote(note) {
+        //console.log("MDLG: Refreshing with note:", note);
+        const { content } = await note.getNoteComplement();
+        //console.log("MDLG: Note content fetched:", content);
+
+        // Check if content is fetched properly
+        if (!content) {
+            //console.log("MDLG: No content fetched. Exiting refresh.");
+            return;
+        }
+
+        const highlightedContent = this.applyHighlighting(content);
+        //console.log("MDLG: Highlighted content:", highlightedContent);
+
+        if (highlightedContent !== content) {
+            //console.log("MDLG: Updating note content with highlighted version");
+
+            // Use the setNoteContent function to update the note content
+            try {
+                await setNoteContent(note.noteId, highlightedContent);
+                //console.log("MDLG: Content updated with highlighted version");
+            } catch (error) {
+                //console.error("MDLG: Error updating content:", error);
+            }
+        } else {
+            //console.log("MDLG: No changes to content, skipping update.");
+        }
+    }
+
+
+ applyHighlighting(content) {
+    //console.log("MDLG: Applying highlighting to content...");
+
+    // Replace <br> with \n, then remove all other HTML tags
+    const plainTextContent = content.replace(/<br\s*\/?>/gi, '\n').replace(/<[^>]*>/g, ' ');
+    //console.log("MDLG: Stripped content:", plainTextContent);
+
+    // Combined regex for date & time
+    const dateTimePattern = /([1-2]\d{3}[-/.](0?[1-9]|1[0-2])[-/.](0[1-9]|[12]\d|3[01]))\s+(\d{1,2}:\d{2})/g;
+    const projectPattern = /\[\S+?\]/g;
+    const contextPattern = /(\s|^)(@\S+)/g;
+    const hashtagPattern = /(\s|^)(#\S+)/g;
+    const positivePattern = /(\s|^)(\+\S+)/g;
+    const negativePattern = /(\s|^)(-\S+)/g;
+
+    // Escape HTML characters to prevent issues with replacements
+    function escapeHtml(str) {
+        return str.replace(/[&<>"']/g, function (char) {
+            return {
+                '&': '&amp;',
+                '<': '&lt;',
+                '>': '&gt;',
+                '"': '&quot;',
+                "'": '&#039;'
+            }[char];
+        });
+    }
+
+      // Helper function to map the number to a colour based on the gradient
+    function getProjectColour(number) {
+        const minColor = { r: 107, g: 109, b: 229 };  // #6b6de5
+        const maxColor = { r: 70, g: 226, b: 96 };   // #46e260
+
+        const ratio = number / 9; // since we are working with 0-9
+
+        // Interpolate the RGB values between minColor and maxColor
+        const r = Math.round(minColor.r + ratio * (maxColor.r - minColor.r));
+        const g = Math.round(minColor.g + ratio * (maxColor.g - minColor.g));
+        const b = Math.round(minColor.b + ratio * (maxColor.b - minColor.b));
+
+        return `rgb(${r}, ${g}, ${b})`;
+    }
+     
+    // Process each line of the plain text content
+    let highlightedLines = plainTextContent.split("\n").map(line => {
+        //console.log("MDLG: Processing line:", line);
+
+        // Apply combined date & time highlight
+        line = line.replace(dateTimePattern, `<span style="color: rgb(188, 55, 237);">$1</span> <span style="color: rgb(188, 55, 237);">$4</span>`);
+
+        // Apply other highlights
+        line = line.replace(projectPattern, (match) => {
+            // Extract the number inside square brackets
+            const number = parseInt(match.match(/\d+/)[0], 10);
+            const colour = getProjectColour(number);
+            return `<span style="color: ${colour}; font-weight: bold;">${escapeHtml(match)}</span>`;
+        });
+        
+        // Apply other highlights
+        //line = line.replace(projectPattern, `<span style="color: rgb(224, 218, 36); font-weight: bold;">$&</span>`);
+        line = line.replace(contextPattern, (_, space, tag) => `${space}<span style="color: rgb(255 165 10);">${escapeHtml(tag)}</span>`);
+        line = line.replace(hashtagPattern, (_, space, tag) => `${space}<span style="color: rgb(41, 229, 242);">${escapeHtml(tag)}</span>`);
+        line = line.replace(positivePattern, (_, space, tag) => `${space}<span style="color: rgb(44, 227, 34);">${escapeHtml(tag)}</span>`);
+        line = line.replace(negativePattern, (_, space, tag) => `${space}<span style="color: rgb(230, 59, 44);">${escapeHtml(tag)}</span>`);
+
+        return line.trim();
+    });
+
+    // Join lines with proper <br> handling
+    let finalContent = highlightedLines.join("<br>");
+
+    //console.log("MDLG: Highlighted content:", finalContent);
+    return finalContent;
+}
+
+
+
+
+    async entitiesReloadedEvent({ loadResults }) {
+        //console.log("MDLG: Entities reloaded event triggered");
+        if (loadResults.isNoteContentReloaded(this.noteId)) {
+            //console.log("MDLG: Note content reloaded, refreshing widget...");
+            //this.refresh();
+        } else {
+            //console.log("MDLG: No content reload detected.");
+        }
+    }
+    
+
+}
+
+console.log("MDLG: Starting Moodlog Highlighter Widget");
+module.exports = MoodlogHighlighterWidget;
+
+function setNoteContent(noteId, content) {
+    /*const noteElement = document.querySelector(`note-detail-readonly-text-content`);
+
+    if (noteElement) {
+        noteElement.innerHTML = content; // Update the displayed content
+        console.log(`MDLG: Updated note ${noteId} in the DOM.`);
+    } else {
+        console.warn(`MDLG: Note element with data-note-id='${noteId}' not found.`);
+    }*/
+    return api.runOnBackend((id, data) => api.getNote(id).setContent(data), [noteId, content]);
+}
diff --git a/highlight_todotxt.js b/highlight_todotxt.js
new file mode 100644
index 0000000..1f1b007
--- /dev/null
+++ b/highlight_todotxt.js
@@ -0,0 +1,157 @@
+const TPL = `<div style="padding: 10px; border-top: 1px solid var(--main-border-color); contain: none;">
+    <strong>Todo Highlighting Applied</strong>
+</div>`;
+
+class MoodlogHighlighterWidget extends api.NoteContextAwareWidget {
+    static get parentWidget() { 
+        //console.log("MDLG: Getting parent widget: center-pane");
+        return 'center-pane'; 
+    }
+
+    get position() { 
+        //console.log("MDLG: Setting position: 100");
+        return 100; 
+    }
+
+    isEnabled() {
+        const enabled = super.isEnabled() 
+            && this.note.type === 'text' 
+            && this.note.hasLabel('todotxt');
+        //console.log(`MDLG: isEnabled: ${enabled}`);
+        return enabled;
+    }
+
+    doRender() {
+        //console.log("MDLG: Rendering widget...");
+        this.$widget = $(TPL);
+        //console.log("MDLG: Widget rendered:", this.$widget);
+        return this.$widget;
+    }
+
+    async refreshWithNote(note) {
+        //console.log("MDLG: Refreshing with note:", note);
+        const { content } = await note.getNoteComplement();
+        //console.log("MDLG: Note content fetched:", content);
+
+        // Check if content is fetched properly
+        if (!content) {
+            //console.log("MDLG: No content fetched. Exiting refresh.");
+            return;
+        }
+
+        const highlightedContent = this.applyHighlighting(content);
+        //console.log("MDLG: Highlighted content:", highlightedContent);
+
+        if (highlightedContent !== content) {
+            //console.log("MDLG: Updating note content with highlighted version");
+
+            // Use the setNoteContent function to update the note content
+            try {
+                await setNoteContent(note.noteId, highlightedContent);
+                //console.log("MDLG: Content updated with highlighted version");
+            } catch (error) {
+                //console.error("MDLG: Error updating content:", error);
+            }
+        } else {
+            //console.log("MDLG: No changes to content, skipping update.");
+        }
+    }
+
+
+applyHighlighting(content) {
+    // Replace <br> with \n, then remove all other HTML tags
+    const plainTextContent = content.replace(/<br\s*\/?\>/gi, '\n').replace(/<[^>]*>/g, ' ');
+
+    // Regular expressions for Todo.txt syntax
+    const datePattern = /\b(0[1-9]|[12]\d|3[01])\/(0[1-9]|1[0-2])\/([1-2]\d{3})\b/g;
+    const completedTaskPattern = /^\s*x\s+.*/g;
+    const priorityPattern = /^(\s*\([A-E]\))\s+/;
+    const projectPattern = /(?:\s|^)(\+\S+)/g;
+    const contextPattern = /(?:\s|^)(@\S+)/g;
+    const waitPattern = /\bWAIT\b/gi;
+    const customAttributePattern = /(\s+[^\s:]+:[^\s:]+)+\s*$/g;
+    
+    function escapeHtml(str) {
+        return str.replace(/[&<>"']/g, char => ({
+            '&': '&amp;', '<': '&lt;', '>': '&gt;',
+            '"': '&quot;', "'": '&#039;'
+        })[char]);
+    }
+
+    let highlightedLines = plainTextContent.split("\n").map(line => {
+        // If line starts with 'x', make the whole line grey
+        if (/^\s*x\s+/.test(line)) {
+            return `<span style="color: grey;">${line}</span>`;
+        }
+        
+        // Highlight priority with different colours based on level
+        line = line.replace(priorityPattern, (match, priority) => {
+            const level = priority.match(/[A-E]/)?.[0];
+            const colors = {
+                'A': 'rgb(255, 0, 0)',       // Red
+                'B': 'rgb(255, 102, 0)',     // Orange
+                'C': 'rgb(255, 204, 0)',     // Yellow
+                'D': 'rgb(0, 153, 255)',     // Blue
+                'E': 'rgb(102, 204, 102)'    // Green
+            };
+            const color = colors[level] || 'black';
+            return `<span style="color: ${color}; font-weight: bold;">${escapeHtml(priority)}</span> `;
+        });
+        
+        // Highlight date
+        line = line.replace(datePattern, (match) => {
+            return `<span style="color: rgb(188, 55, 237);">${escapeHtml(match)}</span>`;
+        });
+
+        // Highlight project tags
+        line = line.replace(projectPattern, (_, tag) => `<span style="color: rgb(224, 218, 36);"> ${escapeHtml(tag)}</span>`);
+        
+        // Highlight context tags
+        line = line.replace(contextPattern, (_, tag) => `<span style="color: rgb(255, 165, 10);"> ${escapeHtml(tag)}</span>`);
+        
+        // Highlight WAIT command
+        line = line.replace(waitPattern, `<span style="color: rgb(255, 69, 0);">WAIT</span>`);
+        
+        // Highlight custom attributes
+        line = line.replace(customAttributePattern, `<span style="color: rgb(100, 149, 237);">$1</span>`);
+        
+        // Remove leading space (if any) from the beginning of the line, without affecting spaces before + or @
+        line = line.replace(/^\s+/, '');
+        
+        return line.trim();
+    });
+
+    return highlightedLines.join("<br>");
+}
+
+
+
+
+
+    async entitiesReloadedEvent({ loadResults }) {
+        //console.log("MDLG: Entities reloaded event triggered");
+        if (loadResults.isNoteContentReloaded(this.noteId)) {
+            //console.log("MDLG: Note content reloaded, refreshing widget...");
+            //this.refresh();
+        } else {
+            //console.log("MDLG: No content reload detected.");
+        }
+    }
+    
+
+}
+
+console.log("TodoHighlight: Starting todotxt Highlighter Widget");
+module.exports = MoodlogHighlighterWidget;
+
+function setNoteContent(noteId, content) {
+    /*const noteElement = document.querySelector(`note-detail-readonly-text-content`);
+
+    if (noteElement) {
+        noteElement.innerHTML = content; // Update the displayed content
+        console.log(`MDLG: Updated note ${noteId} in the DOM.`);
+    } else {
+        console.warn(`MDLG: Note element with data-note-id='${noteId}' not found.`);
+    }*/
+    return api.runOnBackend((id, data) => api.getNote(id).setContent(data), [noteId, content]);
+}
diff --git a/htb2trilium/INSTALL.md b/htb2trilium/INSTALL.md
new file mode 100644
index 0000000..da8afad
--- /dev/null
+++ b/htb2trilium/INSTALL.md
@@ -0,0 +1,41 @@
+## scripts require:
+
+```
+ > pip3 install trilium-py
+```
+
+## config.json
+- get trilium token in trilium options->ETAPI and "create new token"
+- create a new note with a title "Machines"
+- click on this note -> "note info". Note ID is there. This ID goes in "trilium_machines_htb_folder" in the config file
+- set the folder's owned attributes to:
+
+```
+#label:user=promoted,single,text #label:root=promoted,single,text #label:respect=promoted,single,text #user=0 #root=0 #respect=0 
+```
+
+ - create a note named "HTBMachineTemplate" 
+ - set it's "owned attributes" to:
+
+```
+#template #label:User=promoted,single,text #label:Root=promoted,single,text #label:Tags=promoted,single,text
+```
+
+- get this page's ID and put in "trilium_machines_template_id"
+- create "challenges" page, this page's ID goes in: trilium_challenges_folder  
+- create "Sherlocks" page, this page's ID goes in: trilium_sherlocks_folder  
+- create 2 pages "HTBChallengesTemplate" and "HTBSherlocksTemplate", both of these should have the owned attributes:
+
+```
+#template #label:Difficulty=promoted,single,text #label:Solved=promoted,single,text #label:Released=promoted,single,text 
+```
+
+- The ID's of "HTBChallengesTemplate" and "HTBSherlocksTemplate" go in the matching config values.
+
+- to get "in progress" todo colour - add to trilium demo/scripting/taskmanager/implementation/CSS:
+
+```
+ span.fancytree-node.inprogress .fancytree-title {
+    color: orange !important;
+ }
+```
\ No newline at end of file
diff --git a/htb2trilium/README.md b/htb2trilium/README.md
new file mode 100644
index 0000000..1b45ae2
--- /dev/null
+++ b/htb2trilium/README.md
@@ -0,0 +1,16 @@
+htb2trilium
+===============
+
+Pull hackthebox info and stats into personal trilium note taking software
+
+single machine view:
+
+![machine view](images/machine_view.png)
+
+overview of machines:
+
+![machines overview](images/machines_overview.png)
+
+navigation:
+
+![navigation](images/navigation.png)
\ No newline at end of file
diff --git a/htb2trilium/config.json b/htb2trilium/config.json
new file mode 100644
index 0000000..39597ce
--- /dev/null
+++ b/htb2trilium/config.json
@@ -0,0 +1,11 @@
+{
+	"htb_code": "in https://app.hackthebox.com/profile/settings click 'profile settings' and under 'app tokens' you can 'create app token' - that goes here",
+	"trilium_server_url": "https://place.to.your.trilium.server",
+	"trilium_token": "",
+	"trilium_machines_htb_folder": "",
+	"trilium_machines_template_id": "",
+	"trilium_challenges_folder": "",
+	"trilium_challenges_template_id": "",
+	"trilium_sherlocks_folder": "",
+	"trilium_sherlocks_template_id": ""
+}
\ No newline at end of file
diff --git a/htb2trilium/htb2trilium_challenges.py b/htb2trilium/htb2trilium_challenges.py
new file mode 100644
index 0000000..d0d95ca
--- /dev/null
+++ b/htb2trilium/htb2trilium_challenges.py
@@ -0,0 +1,146 @@
+import os
+import json
+import requests
+from collections import defaultdict
+from bs4 import BeautifulSoup
+from datetime import datetime, timezone
+from htb_client import HTBClient
+from trilium_py.client import ETAPI
+
+# Get the absolute path of the script's directory
+script_dir = os.path.dirname(os.path.abspath(__file__))
+
+# Construct the full path to the config.json file
+config_path = os.path.join(script_dir, 'config.json')
+
+# Load configuration from the JSON file
+with open(config_path, 'r') as f:
+    config = json.load(f)
+
+# Accessing config values
+htb_code = config['htb_code']
+trilium_server_url = config['trilium_server_url']
+trilium_token = config['trilium_token']
+trilium_challenges_folder = config['trilium_challenges_folder']
+trilium_challenges_template_id = config['trilium_challenges_template_id']
+
+def get_timestamp(machine):
+    # Parse the release date string into a datetime object
+    release_str = machine['release_date']
+    dt = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ")
+    
+    # Set the timezone to UTC
+    dt = dt.replace(tzinfo=timezone.utc)
+    
+    # Get the timestamp
+    timestamp = dt.timestamp()
+    return timestamp
+
+print("[+] connecting to HTB")
+client = HTBClient(password=htb_code)
+print("[+] connecting to trilium")
+ea = ETAPI(trilium_server_url, trilium_token)
+print("[i] version: ", ea.app_info()['appVersion'])
+
+print("[i] HTB User:", client.user['id'], "-", client.user['name'])
+
+
+print("[+] gathering challenges info")
+categories = defaultdict(list)
+challenges = client.get_all_challenges()
+challenges.sort(key=get_timestamp)
+total_completed = 0  # Variable to track total completed challenges
+
+print(f"[i] Retrieved {len(challenges)} challenges")
+
+# Group challenges by their categories
+for challenge in challenges:
+    categories[challenge['category_name']].append(challenge)
+    if challenge['is_owned']:
+        total_completed += 1  # Increment total completed if challenge is owned
+
+# Print out the grouped challenges with the number of completed and total challenges in each category
+for category, grouped_challenges in categories.items():
+    total = len(grouped_challenges)
+    completed = sum(1 for challenge in grouped_challenges if challenge['is_owned'])  # Count completed challenges
+    
+    res = ea.search_note(
+        search=f"note.title %= '{category}*'",
+        ancestorNoteId=trilium_challenges_folder,
+        ancestorDepth='eq1',
+        limit=1,
+        fastSearch=True,
+    )
+    
+    catId = ""
+    if res['results'] and res['results'][0]['title'].split(' - ')[0].strip().lower() == category.lower():
+        # page exists - lets check if the details have changed
+        ea.patch_note(noteId=res['results'][0]['noteId'], title=category+" - "+str(completed)+" / "+str(total))
+        catId = res['results'][0]['noteId']
+        print(f"[i] updated category: {category} - ({completed}/{total})")
+    else:
+        new_note = ea.create_note(
+            parentNoteId=trilium_challenges_folder,
+            type="text",
+            title=category+" - "+str(completed)+" / "+str(total),
+            content=" ",
+        )
+        catId = new_note['note']['noteId']
+        print(f"[+] created category: {catId} {category} - ({completed}/{total})")
+    
+    for challenge in grouped_challenges:
+        #print(f" - ID: {challenge['id']}, Name: {challenge['name']}, Difficulty: {challenge['difficulty']}")
+        res2 = ea.search_note(
+            search=f"{challenge['name']}",
+            ancestorNoteId=catId,
+            ancestorDepth='eq1',
+            limit=1,
+            fastSearch=True,
+        )
+        # already exists update the values
+        if res2['results'] and res2['results'][0]['title'].lower() == challenge['name'].lower():
+            print(f"[i] found ID: {challenge['id']}, Name: {challenge['name']}, Difficulty: {challenge['difficulty']}")
+            #print(f"Search response for challenge '{challenge['name']}': {res2}")
+            for attribute in res2['results'][0]['attributes']:
+                if attribute['name'] == "Difficulty":
+                    ea.patch_attribute(attributeId=attribute['attributeId'], value=challenge['difficulty'])
+                if attribute['name'] == "Released":
+                    release_str = challenge['release_date']
+                    release_date = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ")
+                    formatted_release_date = release_date.strftime("%d %B %Y")
+                    ea.patch_attribute(attributeId=attribute['attributeId'], value=formatted_release_date)
+                if attribute['name'] == "Solved":
+                    if challenge['is_owned']:
+                        ea.patch_attribute(attributeId=attribute['attributeId'], value="done")
+                    else:
+                        ea.patch_attribute(attributeId=attribute['attributeId'], value=" ")
+                if attribute['name'] == "cssClass":
+                    if challenge['is_owned']:
+                        ea.patch_attribute(attributeId=attribute['attributeId'], value="done")
+                    else:
+                        ea.patch_attribute(attributeId=attribute['attributeId'], value="todo")
+
+        else: # doesnt already exist, create page
+            release_str = challenge['release_date']
+            release_date = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ")
+            formatted_release_date = release_date.strftime("%d %B %Y")
+            new_note = ea.create_note(
+                parentNoteId=catId,
+                type="text",
+                title=challenge['name'],
+                content=" ",
+            )
+            ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="relation", name="template", value=trilium_challenges_template_id)
+            ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Difficulty", value=challenge['difficulty'])
+            ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Released", value=formatted_release_date)
+            if challenge['is_owned']:
+                ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="done")
+                ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Solved", value="done")
+            else:
+                ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="todo")
+                ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Solved", value=" ")
+
+            print(f"[+] created ID: {challenge['id']}, Name: {challenge['name']}, Difficulty: {challenge['difficulty']}")
+
+print("[+] updating folder name                                          ")
+ea.patch_note(noteId=trilium_challenges_folder,title="Challenges - "+str(total_completed)+" / "+str(len(challenges)))
diff --git a/htb2trilium/htb2trilium_machines.py b/htb2trilium/htb2trilium_machines.py
new file mode 100644
index 0000000..de8be53
--- /dev/null
+++ b/htb2trilium/htb2trilium_machines.py
@@ -0,0 +1,203 @@
+import os
+import json
+import requests
+from bs4 import BeautifulSoup
+from datetime import datetime, timezone
+from htb_client import HTBClient
+from trilium_py.client import ETAPI
+
+# Get the absolute path of the script's directory
+script_dir = os.path.dirname(os.path.abspath(__file__))
+
+# Construct the full path to the config.json file
+config_path = os.path.join(script_dir, 'config.json')
+
+# Load configuration from the JSON file
+with open(config_path, 'r') as f:
+    config = json.load(f)
+
+# Accessing config values
+htb_code = config['htb_code']
+trilium_server_url = config['trilium_server_url']
+trilium_token = config['trilium_token']
+trilium_machines_htb_folder = config['trilium_machines_htb_folder']
+trilium_machines_template_id = config['trilium_machines_template_id']
+
+def generate_newpage(machine):
+	no = "<span style=\"color:hsl(0,75%,60%);\">No</span>"
+	yes = "<span style=\"color:hsl(120,75%,60%);\">Yes</span>"
+	user_colour = no
+	if machine['authUserInUserOwns']:
+		user_colour = yes
+	root_colour = no
+	if machine['authUserInRootOwns']:
+		root_colour = yes
+
+	status = "Retired"
+	if machine['active']:
+		status = "Active"
+
+	release_str = machine['release']
+	release_date = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ")
+	formatted_release_date = release_date.strftime("%d %B %Y")
+
+	html = """
+	<figure class="image image-style-align-left image_resized" style="width:12.28%;">
+	  <img src="https://www.hackthebox.com{avatar}">
+	</figure>
+	<p>
+	  <strong>OS:</strong> {os}<br>
+	  <strong>Difficulty:</strong> {difficultyText} <br>
+	  <strong>Rating:</strong> {rating} / 5<br>
+	  <strong>Points:</strong> {points}<br>
+	  <strong>User / Root: </strong> {user_colour} / {root_colour}<br>
+	  <strong>Released:</strong> {release_date}<br>
+	  <strong>State:</strong> {status}
+	</p>
+	<hr>
+	<h2>Notes</h2>
+	<p>&nbsp;</p>
+	""".format(
+		os=machine['os'],
+		difficultyText=machine['difficultyText'],
+		rating=machine['star'],
+		points=machine['points'],
+		release_date=formatted_release_date,
+		user_colour=user_colour,
+		root_colour=root_colour,
+		status = status,
+		avatar = machine['avatar'],
+	)
+
+	return html
+
+def get_timestamp(machine):
+    # Parse the release date string into a datetime object
+    release_str = machine['release']
+    dt = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ")
+    
+    # Set the timezone to UTC
+    dt = dt.replace(tzinfo=timezone.utc)
+    
+    # Get the timestamp
+    timestamp = dt.timestamp()
+    return timestamp
+
+print("[+] connecting to HTB")
+client = HTBClient(password=htb_code)
+print("[+] connecting to trilium")
+ea = ETAPI(trilium_server_url, trilium_token)
+print("[i] version: ", ea.app_info()['appVersion'])
+
+print("[i] HTB User:", client.user['id'], "-", client.user['name'])
+print("[i] user owns:", client.user['user_owns'], "| Root owns:", client.user['root_owns'], "| Respect:", client.user['respects'])
+
+master_folder = ea.get_note(trilium_machines_htb_folder)
+for attribute in master_folder['attributes']:
+	if attribute['name'] == "user":
+		if attribute['value'] != str(client.user['user_owns']):
+			print("[+] updating user owns (folder attribute)")
+			ea.patch_attribute(attributeId=attribute['attributeId'], value=str(client.user['user_owns']))
+	if attribute['name'] == "root":
+		if attribute['value'] != str(client.user['root_owns']):
+			print("[+] updating root owns (folder attribute)")
+			ea.patch_attribute(attributeId=attribute['attributeId'], value=str(client.user['root_owns']))
+	if attribute['name'] == "respect":
+		if attribute['value'] != str(client.user['respects']):
+			print("[+] updating respect (folder attribute)")
+			ea.patch_attribute(attributeId=attribute['attributeId'], value=str(client.user['respects']))
+
+print("[+] gathering machines info")
+machines = client.get_all_machines()
+machines.sort(key=get_timestamp)
+
+print(f"[i] Retrieved {len(machines)} machines")
+#for machine in machines:
+#    print(f" - ID: {machine['id']}, Name: {machine['name']}, OS: {machine['os']}, Difficulty: {machine['difficultyText']}")
+
+machine_count = 0
+completed_count = 0
+for machine in machines:
+	machine_count += 1
+	print('processing: ',machine_count, "/", len(machines), "("+machine['name']+")               " , end='\r')
+
+	res = ea.search_note(
+		search="\""+machine['name']+"\"",
+		ancestorNoteId=trilium_machines_htb_folder,
+		ancestorDepth='eq1',
+		limit=1,
+		fastSearch=True,
+	)
+
+	if res['results'] and machine['name'] == res['results'][0]['title']:
+		# page exists - lets check if the details have changed
+
+		current_html = ea.get_note_content(noteId=res['results'][0]['noteId'])
+		current_soup = BeautifulSoup(current_html, 'html.parser')
+		current_paragraph = current_soup.find_all('p')[0].text
+
+		new_html = generate_newpage(machine)
+		new_soup = BeautifulSoup(new_html, 'html.parser')
+		new_paragraph = new_soup.find_all('p')[0].text
+
+		# current page contains first paragraph of "blank" (useful for when it doesnt create or find the note properly.. shouldnt get here)
+		if current_paragraph == "blank":
+			ea.update_note_content(noteId=res['results'][0]['noteId'], content=new_html)
+			ea.create_attribute(attributeId=None, isInheritable=False, noteId=res['results'][0]['noteId'], type="relation", name="template", value=trilium_machines_htb_folder)
+			if machine['authUserInUserOwns'] and machine['authUserInRootOwns']:
+				ea.create_attribute(attributeId=None, isInheritable=False, noteId=res['results'][0]['noteId'], type="label", name="cssClass", value="done")
+			else:
+				if machine['authUserInUserOwns'] or machine['authUserInRootOwns']:
+					ea.create_attribute(attributeId=None, isInheritable=False, noteId=res['results'][0]['noteId'], type="label", name="cssClass", value="inprogress")
+				else:
+					ea.create_attribute(attributeId=None, isInheritable=False, noteId=res['results'][0]['noteId'], type="label", name="cssClass", value="todo")
+			# re-get the current content
+			current_html = ea.get_note_content(noteId=res['results'][0]['noteId'])
+			current_soup = BeautifulSoup(current_html, 'html.parser')
+
+		if current_paragraph != new_paragraph:
+
+			# details have updated!
+			print("[+] updating page:",machine['name'], "-> "+res['results'][0]['title']+"                 ")
+			replacement = current_soup.find('p')
+			replacement.replace_with( new_soup.find_all('p')[0] )
+			ea.update_note_content(noteId=res['results'][0]['noteId'], content=current_soup)
+
+			# now to update the label
+			for attribute in res['results'][0]['attributes']:
+				if attribute['name'] == "cssClass":
+					if machine['authUserInUserOwns'] and machine['authUserInRootOwns']:
+						ea.patch_attribute(attributeId=attribute['attributeId'], value="done")
+					else:
+						if machine['authUserInUserOwns'] or machine['authUserInRootOwns']:
+							ea.patch_attribute(attributeId=attribute['attributeId'], value="inprogress")
+						else:
+							ea.patch_attribute(attributeId=attribute['attributeId'], value="todo")
+							
+	else:
+		# title does not exist - create the note
+		html = generate_newpage(machine)
+		new_note = ea.create_note(
+			parentNoteId=trilium_machines_htb_folder,
+			type="text",
+			title=machine['name'],
+			content=html,
+		)
+		print("[+] created note:", machine['name'], "                        ")
+
+		ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="relation", name="template", value=trilium_machines_htb_folder)
+		if machine['authUserInUserOwns'] and machine['authUserInRootOwns']:
+			ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="done")
+		else:
+			if machine['authUserInUserOwns'] or machine['authUserInRootOwns']:
+				ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="inprogress")
+			else:
+				ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="todo")
+
+	if machine['authUserInUserOwns'] and machine['authUserInRootOwns']:
+			completed_count += 1
+
+print("[+] updating folder name                                          ")
+ea.patch_note(noteId=trilium_machines_htb_folder,title="Machines - "+str(completed_count)+" / "+str(len(machines)))
+
+print("[=] processed", machine_count)
\ No newline at end of file
diff --git a/htb2trilium/htb2trilium_sherlocks.py b/htb2trilium/htb2trilium_sherlocks.py
new file mode 100644
index 0000000..1e32e59
--- /dev/null
+++ b/htb2trilium/htb2trilium_sherlocks.py
@@ -0,0 +1,151 @@
+import os
+import json
+import requests
+from collections import defaultdict
+from bs4 import BeautifulSoup
+from datetime import datetime, timezone
+from htb_client import HTBClient
+from trilium_py.client import ETAPI
+
+# Get the absolute path of the script's directory
+script_dir = os.path.dirname(os.path.abspath(__file__))
+
+# Construct the full path to the config.json file
+config_path = os.path.join(script_dir, 'config.json')
+
+# Load configuration from the JSON file
+with open(config_path, 'r') as f:
+    config = json.load(f)
+
+# Accessing config values
+htb_code = config['htb_code']
+trilium_server_url = config['trilium_server_url']
+trilium_token = config['trilium_token']
+trilium_sherlocks_folder = config['trilium_sherlocks_folder']
+trilium_sherlocks_template_id = config['trilium_sherlocks_template_id']
+
+def get_timestamp(machine):
+    # Parse the release date string into a datetime object
+    release_str = machine['release_date']
+    dt = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ")
+    
+    # Set the timezone to UTC
+    dt = dt.replace(tzinfo=timezone.utc)
+    
+    # Get the timestamp
+    timestamp = dt.timestamp()
+    return timestamp
+
+print("[+] connecting to HTB")
+client = HTBClient(password=htb_code)
+print("[+] connecting to trilium")
+ea = ETAPI(trilium_server_url, trilium_token)
+print("[i] version: ", ea.app_info()['appVersion'])
+
+print("[i] HTB User:", client.user['id'], "-", client.user['name'])
+
+
+print("[+] gathering sherlocks info")
+categories = defaultdict(list)
+sherlocks = client.get_all_sherlocks()
+sherlocks.sort(key=get_timestamp)
+total_completed = 0  # Variable to track total completed sherlocks
+
+print(f"[i] Retrieved {len(sherlocks)} sherlocks")
+
+# Group sherlocks by their categories
+for sherlock in sherlocks:
+    categories[sherlock['category_name']].append(sherlock)
+    if sherlock['progress'] == 100:
+        total_completed += 1  # Increment total completed if challenge is owned
+
+# Print out the grouped sherlocks with the number of completed and total sherlocks in each category
+for category, grouped_sherlocks in categories.items():
+    total = len(grouped_sherlocks)
+    completed = sum(1 for sherlock in grouped_sherlocks if sherlock['progress'] == 100)  # Count completed challenges
+    
+    res = ea.search_note(
+        search=f"note.title %= '{category}*'",
+        ancestorNoteId=trilium_sherlocks_folder,
+        ancestorDepth='eq1',
+        limit=1,
+        fastSearch=True,
+    )
+    
+    catId = ""
+    if res['results'] and res['results'][0]['title'].split(' - ')[0].strip().lower() == category.lower():
+	# page exists - lets check if the details have changed
+        ea.patch_note(noteId=res['results'][0]['noteId'], title=category+" - "+str(completed)+" / "+str(total))
+        catId = res['results'][0]['noteId']
+        print(f"[i] updated category: {category} - ({completed}/{total})")
+    else:
+        new_note = ea.create_note(
+            parentNoteId=trilium_sherlocks_folder,
+            type="text",
+            title=category+" - "+str(completed)+" / "+str(total),
+            content=" ",
+        )
+        catId = new_note['note']['noteId']
+        print(f"[+] created category: {catId} {category} - ({completed}/{total})")
+    
+    for sherlock in grouped_sherlocks:
+        #print(f" - ID: {challenge['id']}, Name: {challenge['name']}, Difficulty: {challenge['difficulty']}")
+        res2 = ea.search_note(
+            search=f"{sherlock['name']}",
+            ancestorNoteId=catId,
+            ancestorDepth='eq1',
+            limit=1,
+            fastSearch=True,
+        )
+        # already exists update the values
+        if res2['results'] and res2['results'][0]['title'].lower() == sherlock['name'].lower():
+            print(f"[i] found ID: {sherlock['id']}, Name: {sherlock['name']}, Difficulty: {sherlock['difficulty']}")
+    
+            for attribute in res2['results'][0]['attributes']:
+                if attribute['name'] == "Difficulty":
+                    ea.patch_attribute(attributeId=attribute['attributeId'], value=sherlock['difficulty'])
+        
+                if attribute['name'] == "Released":
+                    release_str = sherlock['release_date']
+                    release_date = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ")
+                    formatted_release_date = release_date.strftime("%d %B %Y")
+                    ea.patch_attribute(attributeId=attribute['attributeId'], value=formatted_release_date)
+        
+                if attribute['name'] == "Solved":
+                    ea.patch_attribute(attributeId=attribute['attributeId'], value=str(sherlock['progress']))
+        
+                if attribute['name'] == "cssClass":
+                    # Determine the cssClass value based on the progress
+                    if sherlock['progress'] == 0:
+                        ea.patch_attribute(attributeId=attribute['attributeId'], value="todo")
+                    elif sherlock['progress'] == 100:
+                        ea.patch_attribute(attributeId=attribute['attributeId'], value="done")
+                    elif 0 < sherlock['progress'] < 100:
+                        ea.patch_attribute(attributeId=attribute['attributeId'], value="inprogress")
+
+
+        else: # doesnt already exist, create page
+            release_str = sherlock['release_date']
+            release_date = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ")
+            formatted_release_date = release_date.strftime("%d %B %Y")
+            new_note = ea.create_note(
+                parentNoteId=catId,
+                type="text",
+                title=sherlock['name'],
+                content=" ",
+            )
+            ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="relation", name="template", value=trilium_sherlocks_template_id)
+            ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Difficulty", value=sherlock['difficulty'])
+            ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Released", value=formatted_release_date)
+            ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Solved", value=str(sherlock['progress']))
+            if sherlock['progress'] == 0:
+                ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="todo")
+            elif sherlock['progress'] == 100:
+                ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="done")
+            elif 0 < sherlock['progress'] < 100:
+                ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="inprogress")
+
+            print(f"[+] created ID: {sherlock['id']}, Name: {sherlock['name']}, Difficulty: {sherlock['difficulty']}")
+
+print("[+] updating folder name                                          ")
+ea.patch_note(noteId=trilium_sherlocks_folder,title="Sherlocks - "+str(total_completed)+" / "+str(len(sherlocks)))
diff --git a/htb2trilium/htb_client.py b/htb2trilium/htb_client.py
new file mode 100644
index 0000000..a35929a
--- /dev/null
+++ b/htb2trilium/htb_client.py
@@ -0,0 +1,214 @@
+import requests
+import warnings
+
+warnings.filterwarnings("ignore")
+
+class HTBClient:
+    def __init__(self, password):
+        self.password = password
+        self.base_url = 'https://labs.hackthebox.com/api/v4'
+        self.proxies = {
+            #"http": "http://127.0.0.1:8080",  # Burp proxy for HTTP traffic
+            #"https": "http://127.0.0.1:8080"  # Burp proxy for HTTPS traffic
+        }
+        # Fetch user info
+        self.user = self.get_user_info()
+        
+        # Fetch user stats and store them in self.user
+        user_owns, root_owns, respects = self.get_user_stats(self.user['id'])
+        self.user['user_owns'] = user_owns
+        self.user['root_owns'] = root_owns
+        self.user['respects'] = respects
+
+    def get_user_info(self):
+        headers = {
+            "Authorization": f"Bearer {self.password}",
+            "User-Agent": None  # Explicitly remove User-Agent
+        }
+        response = requests.get(
+            f'{self.base_url}/user/info', 
+            headers=headers, 
+            proxies=self.proxies,
+            verify=False  # Disable SSL verification
+        )
+        if response.status_code != 200:
+            raise Exception(f"Error fetching {self.base_url}/user/info user info: {response.status_code}, {response.text}")
+        
+        # Return the user info as a dictionary
+        data = response.json().get('info')
+        return {'id': data['id'], 'name': data['name'], 'email': data['email']}
+
+    def get_user_stats(self, user_id):
+        headers = {
+            "Authorization": f"Bearer {self.password}",
+            "User-Agent": None  # Explicitly remove User-Agent
+        }
+        response = requests.get(
+            f'{self.base_url}/user/profile/basic/{user_id}', 
+            headers=headers, 
+            proxies=self.proxies,
+            verify=False  # Disable SSL verification
+        )
+        if response.status_code != 200:
+            raise Exception(f"Error fetching user stats: {response.status_code}, {response.text}")
+        
+        # Extract user statistics from the response
+        data = response.json().get('profile')
+        user_owns = data['user_owns']
+        root_owns = data['system_owns']
+        respects = data['respects']
+        return user_owns, root_owns, respects
+
+    def get_active_machines(self):
+        machines = []
+        seen_ids = set()  # Track unique machine IDs
+        seen_names = set()  # Track unique machine names
+        page = 1
+
+        while True:
+            response = requests.get(
+                f'{self.base_url}/machine/paginated?per_page=100&page={page}',
+                headers={
+                    "Authorization": f"Bearer {self.password}",
+                    "User-Agent": None  # Explicitly remove User-Agent
+                },
+                proxies=self.proxies,
+                verify=False  # Disable SSL verification
+            )
+
+            if response.status_code != 200:
+                raise Exception(f"Error fetching active machines: {response.status_code}, {response.text}")
+
+            data = response.json()
+            for machine in data['data']:
+                if machine['id'] not in seen_ids and machine['name'] not in seen_names:
+                    machines.append(machine)
+                    seen_ids.add(machine['id'])
+                    seen_names.add(machine['name'])
+
+            # Check for pagination
+            if page >= data['meta']['last_page']:
+                break
+            page += 1
+        
+        return machines
+
+    def get_retired_machines(self):
+        machines = []
+        seen_ids = set()  # Track unique machine IDs
+        seen_names = set()  # Track unique machine names
+        page = 1
+
+        while True:
+            response = requests.get(
+                f'{self.base_url}/machine/list/retired/paginated?per_page=100&page={page}',
+                headers={
+                    "Authorization": f"Bearer {self.password}",
+                    "User-Agent": None  # Explicitly remove User-Agent
+                },
+                proxies=self.proxies,
+                verify=False  # Disable SSL verification
+            )
+
+            if response.status_code != 200:
+                raise Exception(f"Error fetching retired machines: {response.status_code}, {response.text}")
+
+            data = response.json()
+            for machine in data['data']:
+                if machine['id'] not in seen_ids and machine['name'] not in seen_names:
+                    machines.append(machine)
+                    seen_ids.add(machine['id'])
+                    seen_names.add(machine['name'])
+
+            # Check for pagination
+            if page >= data['meta']['last_page']:
+                break
+            page += 1
+        
+        return machines
+
+    def get_all_machines(self):
+        # Combine active and retired machines, ensuring no duplicates
+        active_machines = self.get_active_machines()
+        retired_machines = self.get_retired_machines()
+
+        all_machines = active_machines + retired_machines
+        seen_ids = set()  # Track unique machine IDs
+        seen_names = set()  # Track unique machine names
+        unique_machines = []
+
+        for machine in all_machines:
+            if machine['id'] not in seen_ids and machine['name'] not in seen_names:
+                unique_machines.append(machine)
+                seen_ids.add(machine['id'])
+                seen_names.add(machine['name'])
+
+        return unique_machines
+
+    def get_all_challenges(self):
+        challenges = []
+        seen_ids = set()  # Track unique machine IDs
+        seen_names = set()  # Track unique machine names
+        page = 1
+
+        while True:
+            response = requests.get(
+                f'{self.base_url}/challenges?per_page=100&page={page}',
+                headers={
+                    "Authorization": f"Bearer {self.password}",
+                    "User-Agent": None  # Explicitly remove User-Agent
+                },
+                proxies=self.proxies,
+                verify=False  # Disable SSL verification
+            )
+
+            if response.status_code != 200:
+                raise Exception(f"Error fetching challenges: {response.status_code}, {response.text}")
+
+            data = response.json()
+            for challenge in data['data']:
+                if challenge['id'] not in seen_ids and challenge['name'] not in seen_names:
+                    challenges.append(challenge)
+                    seen_ids.add(challenge['id'])
+                    seen_names.add(challenge['name'])
+
+            # Check for pagination
+            if page >= data['meta']['last_page']:
+                break
+            page += 1
+        
+        return challenges
+
+    def get_all_sherlocks(self):
+        sherlocks = []
+        seen_ids = set()  # Track unique machine IDs
+        seen_names = set()  # Track unique machine names
+        page = 1
+
+        while True:
+            response = requests.get(
+                f'{self.base_url}/sherlocks?per_page=100&page={page}',
+                headers={
+                    "Authorization": f"Bearer {self.password}",
+                    "User-Agent": None  # Explicitly remove User-Agent
+                },
+                proxies=self.proxies,
+                verify=False  # Disable SSL verification
+            )
+
+            if response.status_code != 200:
+                raise Exception(f"Error fetching sherlocks: {response.status_code}, {response.text}")
+
+            data = response.json()
+            for sherlock in data['data']:
+                if sherlock['id'] not in seen_ids and sherlock['name'] not in seen_names:
+                    sherlocks.append(sherlock)
+                    seen_ids.add(sherlock['id'])
+                    seen_names.add(sherlock['name'])
+
+            # Check for pagination
+            if page >= data['meta']['last_page']:
+                break
+            page += 1
+        
+        return sherlocks
\ No newline at end of file
diff --git a/htb2trilium/images/machine_view.png b/htb2trilium/images/machine_view.png
new file mode 100755
index 0000000..5bcb448
--- /dev/null
+++ b/htb2trilium/images/machine_view.png
Binary files differ
diff --git a/htb2trilium/images/machines_overview.png b/htb2trilium/images/machines_overview.png
new file mode 100755
index 0000000..2d84760
--- /dev/null
+++ b/htb2trilium/images/machines_overview.png
Binary files differ
diff --git a/htb2trilium/images/navigation.png b/htb2trilium/images/navigation.png
new file mode 100755
index 0000000..fda8624
--- /dev/null
+++ b/htb2trilium/images/navigation.png
Binary files differ
diff --git a/trilium_sync.php b/trilium_sync.php
new file mode 100644
index 0000000..9537759
--- /dev/null
+++ b/trilium_sync.php
@@ -0,0 +1,740 @@
+<?php
+
+// Configuration
+$apiToken = ""; // API Token (menu->options, ETAPI)
+$serverUrl = "http://127.0.0.1:8181"; // location to your trilium instance
+$parentNoteId = ""; // Change this to the parent note ID you want to list sub-notes for (The main folder/note)
+$localFolder = '/NAS/Docs/trilium-data/'; // folder to sync to
+
+// leave below here
+$deletedFolders = array();
+$deletedFiles = array();
+
+function getTriliumVersion($apiToken, $serverUrl) {
+    $url = "$serverUrl/etapi/app-info";
+    
+    $ch = curl_init($url);
+    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+    curl_setopt($ch, CURLOPT_HTTPHEADER, [
+        "Authorization: $apiToken",
+        "Content-Type: application/json"
+    ]);
+    
+    $response = curl_exec($ch);
+    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+    curl_close($ch);
+    
+    if ($httpCode !== 200) {
+        die("Error fetching notes: HTTP $httpCode\n$response");
+    }
+    
+    return json_decode($response, true);
+}
+
+function checkFolderAccess($folderPath) {
+    if (!is_dir($folderPath) || !is_readable($folderPath)) {
+        die("Error: Cannot access folder '$folderPath'.");
+    }
+}
+
+function fetchSubNotes($parentNoteId, $apiToken, $serverUrl) {
+    $url = "$serverUrl/etapi/notes/$parentNoteId";
+    
+    $ch = curl_init($url);
+    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+    curl_setopt($ch, CURLOPT_HTTPHEADER, [
+        "Authorization: $apiToken",
+        "Content-Type: application/json"
+    ]);
+    
+    $response = curl_exec($ch);
+    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+    curl_close($ch);
+    
+    if ($httpCode !== 200) {
+        die("Error fetching notes: HTTP $httpCode\n$response");
+    }
+    
+    return json_decode($response, true);
+}
+
+function fetchNoteContents($noteId, $apiToken, $serverUrl) {
+    $url = "$serverUrl/etapi/notes/$noteId/content";
+    
+    $ch = curl_init($url);
+    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+    curl_setopt($ch, CURLOPT_HTTPHEADER, [
+        "Authorization: $apiToken",
+        "Content-Type: application/json"
+    ]);
+    
+    $response = curl_exec($ch);
+    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+    curl_close($ch);
+    
+    if ($httpCode !== 200) {
+        return "Error fetching notes: HTTP $httpCode\n$response";
+    }
+
+    $response = convertHtmlToText($response);
+ 
+    return $response;
+}
+
+function updateNoteContents($noteId, $content, $apiToken, $serverUrl) {
+    $url = "$serverUrl/etapi/notes/$noteId/content";
+
+    $html = convertTextToHtml($content);
+
+    // Initialise cURL request
+    $ch = curl_init($url);
+    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "PUT");  // Set the request method to PUT
+    curl_setopt($ch, CURLOPT_HTTPHEADER, [
+        "Authorization: $apiToken",
+        "Content-Type: text/plain"
+    ]);
+    curl_setopt($ch, CURLOPT_POSTFIELDS, $html);  // Add formatted HTML to the body
+    
+    $response = curl_exec($ch);
+    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+    curl_close($ch);
+    
+    if ($httpCode !== 204) {
+        die("Error updating note: HTTP $httpCode\n$response");
+    }
+
+    return;
+}
+
+function createNote($parentNoteId, $title, $content, $apiToken, $serverUrl) {
+    $url = "$serverUrl/etapi/create-note";
+    
+    $data = json_encode([
+        "parentNoteId" => $parentNoteId,
+        "title" => $title,
+        "type" => "text",
+        "content" => $content
+    ]);
+    
+    $ch = curl_init($url);
+    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+    curl_setopt($ch, CURLOPT_HTTPHEADER, [
+        "Authorization: $apiToken",
+        "Content-Type: application/json"
+    ]);
+    curl_setopt($ch, CURLOPT_POST, true);
+    curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
+    
+    $response = curl_exec($ch);
+    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+    curl_close($ch);
+
+    $response = json_decode($response, true);
+    return $response['note'];
+}
+
+function deleteNote($noteId, $apiToken, $serverUrl){
+    $url = "$serverUrl/etapi/notes/$noteId";
+
+    $ch = curl_init($url);
+    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+    curl_setopt($ch, CURLOPT_HTTPHEADER, [
+        "Authorization: $apiToken",
+        "Content-Type: application/json"
+    ]);
+    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "DELETE");
+
+    $response = curl_exec($ch);
+    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+    curl_close($ch);
+
+    if ($httpCode === 204) {
+        return true; // Deletion successful
+    }
+    
+    return json_decode($response, true); // Return JSON response for errors
+}
+
+function fetchAllNotes($noteId, $apiToken, $serverUrl, $localFolder, $path = '') {
+    $note = fetchSubNotes($noteId, $apiToken, $serverUrl);
+    
+    if (!empty($note['childNoteIds'])) {
+        $currentPath = $path . $note['title'] . '/';
+
+        $fullPath = rtrim($localFolder, '/') . '/' . ltrim($path, '/') . '/' . ltrim($note['title'], '/');
+        $fullPath = preg_replace('/\/+/', '/', $fullPath);
+
+        if (is_dir($fullPath)) {
+            $status = 'Exists';
+            $modifiedDisk = stat($fullPath)['mtime'];
+        } else {
+            $status = 'Missing_Disk';
+            $modifiedDisk = "";
+        }
+        
+        $folder = [
+            'noteId' => $noteId,
+            'folder' => $note['title'],
+            'dateModifiedTrilium' => convertToUnixTimestamp($note['dateModified']),
+            'dateModifiedDisk' => $modifiedDisk,
+            'parent' => $note['parentNoteIds'][0],
+            'children' => [],
+            'status' => $status
+        ];
+        
+        foreach ($note['childNoteIds'] as $childId) {
+            $folder['children'][] = fetchAllNotes($childId, $apiToken, $serverUrl, $localFolder, $currentPath);
+        }
+        
+        return $folder;
+    } else {
+
+        $fullPath = $localFolder . $path . $note['title'];
+        //echo "does it exist?: $fullPath\n";
+        $fileExists = file_exists($fullPath) ? 'Exists' : 'Missing_Disk';
+
+        if ($fileExists === 'Exists' && is_file($fullPath)) {
+            $modifiedDisk = convertToUnixTimestamp(filemtime($fullPath));
+            $hashDisk = md5(file_get_contents($fullPath));
+        } else {
+            $modifiedDisk = "";
+            $hashDisk = "";
+        }
+
+        $hashTrilium = md5(fetchNoteContents($noteId, $apiToken, $serverUrl));
+        
+        return [
+            'noteId' => $noteId,
+            'file' => $note['title'],
+            'path' => $fullPath,
+            'dateModifiedTrilium' => convertToUnixTimestamp($note['dateModified']),
+            'dateModifiedDisk' => $modifiedDisk,
+            'parent' => $note['parentNoteIds'][0],
+            'hashTrilium' => $hashTrilium,
+            'hashDisk' => $hashDisk,
+            'hashMach' => "",
+            'status' => $fileExists
+        ];
+    }
+}
+
+function scanLocalFiles($directory, $basePath, $path = '') {
+    $files = [];
+    $items = scandir($directory);
+
+    foreach ($items as $item) {
+        if ($item === '.' || $item === '..') continue;
+
+        $fullPath = rtrim($directory, '/') . '/' . $item;
+        $relativePath = str_replace($basePath, '', $fullPath);
+        $currentPath = rtrim($path, '/') . '/' . $item;
+
+        if (is_dir($fullPath)) {
+            $modifiedDisk = stat($fullPath)['mtime'];
+            $files[] = [
+                'noteId' => "",
+                'folder' => $item,
+                'dateModifiedTrilium' => "",
+                'dateModifiedDisk' => $modifiedDisk,
+                'children' => scanLocalFiles($fullPath, $basePath, $currentPath), // Recursive call for directories
+                'parent' => "",
+                'status' => 'Missing_Trilium',
+            ];
+        } else {
+            $hashDisk = md5(file_get_contents($fullPath));
+            $files[] = [
+                'noteId' => "",
+                'file' => $item,
+                'dateModifiedTrilium' => "",
+                'dateModifiedDisk' => convertToUnixTimestamp(filemtime($fullPath)),
+                'status' => 'Missing_Trilium',
+                'parent' => "",
+                'hashTrilium' => '',
+                'hashDisk' => $hashDisk,
+                'hashMach' => "",
+                'path' => ltrim($relativePath, '/'),
+            ];
+        }
+    }
+
+    return $files;
+}
+
+function mergeStructures(&$triliumStructure, $localFiles) {
+    $triliumPaths = [];
+    
+    if (isset($triliumStructure['children'])) {
+        foreach ($triliumStructure['children'] as &$child) {
+            $key = isset($child['folder']) ? $child['folder'] : $child['file'];
+            $triliumPaths[$key] = &$child;
+        }
+    } else {
+        $triliumStructure['children'] = [];
+    }
+    
+    foreach ($localFiles as $localItem) {
+        $key = isset($localItem['folder']) ? $localItem['folder'] : $localItem['file'];
+        
+        if (!isset($triliumPaths[$key])) {
+            if (isset($localItem['path'])) {
+                $localItem['path'] = preg_replace('|^[^/]+/|', '', $localItem['path']);
+            }
+            $triliumStructure['children'][] = $localItem;
+        } else if (isset($localItem['children'])) {
+            mergeStructures($triliumPaths[$key], $localItem['children']);
+        }
+    }
+}
+
+function compareFileHashes(&$item) {
+    // If the item is an array, check if it has children
+    if (isset($item['children']) && is_array($item['children'])) {
+        foreach ($item['children'] as &$child) {
+            compareFileHashes($child);
+        }
+    }
+
+    // Perform hash comparison only if it's a leaf node (has 'file' but no 'children')
+    if (isset($item['file']) && (!isset($item['children']) || empty($item['children']))) {
+        if (!empty($item['hashTrilium']) && !empty($item['hashDisk'])) {
+            if ($item['hashTrilium'] === $item['hashDisk']) {
+                $item['hashMatch'] = "Match";
+            } else { 
+                $item['hashMatch'] = 'Diff'; 
+                if($item['dateModifiedTrilium'] < $item['dateModifiedDisk']){ $item['status'] ="Update_Trilium"; }
+                if($item['dateModifiedTrilium'] > $item['dateModifiedDisk']){ $item['status'] ="Update_Disk"; }
+            }
+        } else {
+            $item['hashMatch'] = 'N/A';
+        }
+
+    }
+}
+
+function createTriliumFolder(&$item, $path = '',$parentNoteId = null) {
+    global $localFolder, $apiToken, $serverUrl;
+    global $deletedFolders, $deletedFiles; 
+    // Assign the parent's noteId as the parentId for the current item
+
+    if (isset($item['folder']) && strpos($item['folder'], '[del]') !== 0  ) {
+
+        $startsWithDeleted = false;
+        foreach ($deletedFolders as $deletedFolder) {
+            if (strncmp($path . $item['folder'] ."/", $deletedFolder, strlen($deletedFolder)) === 0) {
+                $startsWithDeleted = true;
+                break;
+            }
+        }
+
+        if (!empty($item['status']) && $item['status'] == "Missing_Trilium" ) {
+            if(!$startsWithDeleted){
+                $fullpath =  $path . $item['folder'] . '/';
+                echo "[+] creating trilium folder: $fullpath\n";
+                
+                $newId = createNote($parentNoteId, $item['folder'], "", $apiToken, $serverUrl);
+                // Update item with new noteId
+                $item['noteId'] = $newId['noteId'];
+                $item['dateModifiedTrilium'] = time();
+                $item['status'] = "Updated";
+            } else{
+                $item['status'] = "Skipped";
+            }
+        }
+    }
+    
+    // Update parentId for immediate children
+    if (isset($item['children']) && is_array($item['children'])) {
+        foreach ($item['children'] as &$child) {
+            if (isset($item['noteId']) && !empty($item['noteId'])) {
+                $child['parent'] = $item['noteId'];
+            }
+            $currentPath = $path . $item['folder'] . '/';
+            createTriliumFolder($child, $currentPath, $item['noteId']);
+        }
+    }
+}
+
+function createTriliumFile(&$item, $path = '',$parentNoteId = null) {
+    global $localFolder, $apiToken, $serverUrl;
+    global $deletedFolders, $deletedFiles; 
+
+    if (isset($item['file']) && strpos($item['file'], '[del]') !== 0) {
+
+        $startsWithDeleted = false;
+        foreach ($deletedFolders as $deletedFolder) {
+            if (strncmp($path , $deletedFolder, strlen($deletedFolder)) === 0) {
+                $startsWithDeleted = true;
+                break;
+            }
+        }
+        foreach ($deletedFiles as $deletedFile) {
+            if (strncmp($path . $item['file'] , $deletedFile, strlen($deletedFile)) === 0) {
+                $startsWithDeleted = true;
+                break;
+            }
+        }
+
+        if (!empty($item['status']) && $item['status'] == "Missing_Trilium") {
+            if(!$startsWithDeleted){
+                $fullpath =  $localFolder . $path . $item['file'] ;
+                echo "[+] creating trilium note: $fullpath\n";
+                
+                $contents = file_get_contents($fullpath);
+                $newId = createNote($parentNoteId, $item['file'], $contents, $apiToken, $serverUrl);
+                // Update item with new noteId
+                $item['noteId'] = $newId['noteId'];
+                $item['dateModifiedTrilium'] = time();
+                $item['status'] = "Updated";
+            }else{ $item['status'] = "Skipped"; }
+        }
+    }
+    
+    // Update parentId for immediate children
+    if (isset($item['children']) && is_array($item['children'])) {
+        foreach ($item['children'] as &$child) {
+            if (isset($item['noteId']) && !empty($item['noteId'])) {
+                $child['parent'] = $item['noteId'];
+            }
+            $currentPath = $path . $item['folder'] . '/';
+            createTriliumFile($child, $currentPath, $item['noteId']);
+        }
+    }
+}
+
+function createDiskFileAndFolder(&$item, $path = '', $parentNoteId = null) {
+    global $localFolder, $apiToken, $serverUrl;
+    global $deletedFolders, $deletedFiles; 
+
+    // Assign the parent's noteId as the parentId for the current item
+    if (isset($item['folder']) && strpos($item['folder'], '[del]') !== 0) {
+        if (!empty($item['status']) && $item['status'] == "Missing_Disk") {
+            $fullpath = $localFolder . $path . $item['folder'] . '/';
+            echo "[+] creating disk folder: $fullpath\n";
+            
+            mkdir($fullpath, 0777, true);
+            $item['dateModifiedDisk'] = time();
+            $item['status'] = "Updated";
+        }
+    }
+
+    if (isset($item['file']) && strpos($item['file'], '[del]') !== 0) {
+
+        $startsWithDeleted = false;
+        foreach ($deletedFolders as $deletedFolder) {
+            if (strncmp($path , $deletedFolder, strlen($deletedFolder)) === 0) {
+                $startsWithDeleted = true;
+                break;
+            }
+        }
+        foreach ($deletedFiles as $deletedFile) {
+            if (strncmp($path . $item['file'] , $deletedFile, strlen($deletedFile)) === 0) {
+                $startsWithDeleted = true;
+                break;
+            }
+        }
+
+        if (!empty($item['status']) && $item['status'] == "Missing_Disk") {
+            if(!$startsWithDeleted){
+                $fullpath =  $localFolder . $path . $item['file'] ;
+                echo "[+] creating disk note: $fullpath\n";
+                
+                file_put_contents($fullpath, fetchNoteContents($item['noteId'], $apiToken, $serverUrl) );
+                $item['dateModifiedDisk'] = time();
+                $item['status'] = "Updated";
+            }else{ $item['status'] = "Skipped"; }
+        }
+    }
+  
+    // Update parentId for immediate children
+    if (isset($item['children']) && is_array($item['children'])) {
+        foreach ($item['children'] as &$child) {
+            if (isset($item['noteId']) && !empty($item['noteId'])) {
+                $child['parent'] = $item['noteId'];
+            }
+            $currentPath = $path . $item['folder'] . '/';
+            createDiskFileAndFolder($child, $currentPath, $item['noteId']);
+        }
+    }
+}
+
+function deleteItem(&$item, $path = '', $parentNoteId = null) {
+    global $localFolder, $apiToken, $serverUrl;
+    global $deletedFolders, $deletedFiles; 
+    // Assign the parent's noteId as the parentId for the current item
+    if (isset($item['folder']) && strpos($item['folder'], '[del]') === 0) {
+        $item['status'] = "Delete";
+        
+        $item_fixed = trim(substr($item['folder'], 5)); // Remove "[del]" from the start
+        $fullpath = $localFolder . $path .  $item_fixed . '/';
+        echo "[-] deleting disk folder: $fullpath\n";
+        $deletedFolders[] = $path .  $item_fixed . '/';
+        $deletedFolders[] = $path .  $item['folder'] . '/';
+
+        deleteFolderAndContents($fullpath);
+        deleteNote($item['noteId'], $apiToken, $serverUrl);
+    }
+    
+    if (isset($item['file']) && strpos($item['file'], '[del]') === 0) {
+        $item['status'] = "Delete";
+
+        $item_fixed = trim(substr($item['file'], 5)); // Remove "[del]" from the start
+        $fullpath = $localFolder . $path .  $item_fixed ;
+        echo "[-] deleting disk file: $fullpath\n";
+        $deletedFiles[] = $path .  $item_fixed;
+        $deletedFiles[] = $path .  $item['file'];
+
+        deleteNote($item['noteId'], $apiToken, $serverUrl);
+        if (file_exists($fullpath)) {
+            if (is_file($fullpath)) {
+                unlink($fullpath);
+            } elseif (is_dir($fullpath)) {
+                foreach (array_diff(scandir($fullpath), ['.', '..']) as $file) {
+                    unlink("$fullpath") ?: rmdir("$fullpath");
+                }
+                rmdir($fullpath);
+            }
+        }
+
+    }
+    
+    // Update parentId for immediate children
+    if (isset($item['children']) && is_array($item['children'])) {
+        foreach ($item['children'] as &$child) {
+            if (isset($item['folder']) && strpos($item['folder'], '[del]') === 0)
+                $child['status'] == "Delete";
+            $currentPath = $path . $item['folder'] . '/';
+            deleteItem($child, $currentPath, $item['noteId']);
+        }
+    }
+}
+
+function deleteFolderAndContents($folderPath) {
+    if (!is_dir($folderPath)) {
+        return false; // Ensure it's a directory
+    }
+
+    // Scan the directory for all files and subdirectories
+    $files = array_diff(scandir($folderPath), array('.', '..'));
+
+    foreach ($files as $file) {
+        $filePath = $folderPath . DIRECTORY_SEPARATOR . $file;
+
+        if (is_dir($filePath)) {
+            deleteFolderAndContents($filePath);
+        } else {
+
+            unlink($filePath);
+        }
+    }
+    return rmdir($folderPath);
+}
+
+function updateExistingFile(&$item, $path = '', $parentNoteId = null){
+    global $localFolder, $apiToken, $serverUrl;
+
+    if (isset($item['file'])) {
+        if (!empty($item['status']) && $item['status'] == "Update_Trilium") {
+            $fullpath =  $localFolder . $path . $item['file'] ;
+            echo "[+] updating file on trilium: $fullpath\n";
+            
+            $content = file_get_contents($fullpath);
+            updateNoteContents($item['noteId'], $content, $apiToken, $serverUrl);
+            $item['dateModifiedTrilium'] = time();
+            $item['status'] = "Updated";
+        }
+    }
+
+    if (isset($item['file'])) {
+        if (!empty($item['status']) && $item['status'] == "Update_Disk") {
+            $fullpath =  $localFolder . $path . $item['file'] ;
+            echo "[+] updating file on disk: $fullpath\n";
+            
+            file_put_contents($fullpath, fetchNoteContents($item['noteId'], $apiToken, $serverUrl) );
+            $item['dateModifiedDisk'] = time();
+            $item['status'] = "Updated";
+        }
+    }
+    
+    // Update parentId for immediate children
+    if (isset($item['children']) && is_array($item['children'])) {
+        foreach ($item['children'] as &$child) {
+            $currentPath = $path . $item['folder'] . '/';
+            updateExistingFile($child, $currentPath, $item['noteId']);
+        }
+    }
+}
+
+function printFolderTable($structure, $path = '', $printHeader = true) {
+    // Define column widths to ensure consistent formatting
+    $columnWidths = [
+        'path' => 25,
+        'folder' => 25,
+        'parent' => 13,
+        'noteId' => 13,
+        'dateModifiedTrilium' => 11,
+        'dateModifiedDisk' => 11,
+        'status' => 15
+    ];
+
+    if ($printHeader) {
+        echo "- Folders -\n";
+        echo formatColumn("Folder/Path", 25) . 
+             formatColumn("Folder Name", 25) . 
+             formatColumn("Parent", 13) . 
+             formatColumn("noteId", 13) . 
+             formatColumn("Mod Trl", 11) . 
+             formatColumn("Mod Dsk", 11) . 
+             formatColumn("Status", 15) . "\n";
+    }
+
+    if (isset($structure['folder'])) {
+        $currentPath = $path . $structure['folder'] . '/';
+        
+        // Print the folder details
+        echo formatColumn($currentPath, $columnWidths['path']) . 
+             formatColumn($structure['folder'], $columnWidths['folder']) .
+             formatColumn($structure['parent'], $columnWidths['parent']) . 
+             formatColumn($structure['noteId'], $columnWidths['noteId']) .
+             formatColumn($structure['dateModifiedTrilium'], $columnWidths['dateModifiedTrilium']) . 
+             formatColumn($structure['dateModifiedDisk'], $columnWidths['dateModifiedDisk']) .
+             formatColumn($structure['status'], $columnWidths['status']) . "\n"; 
+        
+        // Recursively process children
+        foreach ($structure['children'] as $child) {
+            printFolderTable($child, $currentPath, false);
+        }
+    }
+}
+
+function printFileTable($structure, $path = '', $printHeader = true) {
+    // Define column widths to ensure consistent formatting
+    $columnWidths = [
+        'path' => 25,
+        'parent' => 13,
+        'noteId' => 13,
+        'file' => 25,
+        'dateModifiedTrilium' => 11,
+        'dateModifiedDisk' => 11,
+        'hashMatch' => 6,
+        'status' => 15
+    ];
+
+    if ($printHeader) {
+        echo "- Files -\n";
+        echo formatColumn("Folder/Path", 25) . 
+             formatColumn("Filename", 25) . 
+             formatColumn("Parent", 13) .
+             formatColumn("noteId", 13) . 
+             formatColumn("Mod Trl", 11) . 
+             formatColumn("Mod Dsk", 11) . 
+             formatColumn("Hash", 6) . 
+             formatColumn("Status", 15) . "\n";
+    }
+
+    if (isset($structure['folder'])) {
+        $currentPath = $path . $structure['folder'] . '/';
+        foreach ($structure['children'] as $child) {
+            printFileTable($child, $currentPath, false);
+        }
+    } else {
+        // Format and print the table row with aligned columns
+        echo formatColumn($path, $columnWidths['path']) . 
+             formatColumn($structure['file'], $columnWidths['file']) . 
+             formatColumn($structure['parent'], $columnWidths['parent']) . 
+             formatColumn($structure['noteId'], $columnWidths['noteId']) . 
+             formatColumn($structure['dateModifiedTrilium'], $columnWidths['dateModifiedTrilium']) . 
+             formatColumn($structure['dateModifiedDisk'], $columnWidths['dateModifiedDisk']) . 
+             formatColumn($structure['hashMatch'], $columnWidths['hashMatch']) .
+             formatColumn($structure['status'], $columnWidths['status']) . "\n";
+    }
+}
+
+function convertToUnixTimestamp($timestamp) {
+    // If timestamp is already an integer (Unix timestamp), return it directly
+    if (is_int($timestamp)) {
+        return $timestamp;
+    }
+    
+    // For other formats, convert to Unix timestamp
+    $date = new DateTime($timestamp);
+    return $date->getTimestamp();
+}
+
+function convertTextToHtml($text) {
+    // 1: Plain Text Search
+    $text = str_replace("&", "&amp;", $text);
+    $text = str_replace("<", "&lt;", $text);
+    $text = str_replace(">", "&gt;", $text);
+
+    // 2: Convert Tabs to 4 &nbsp;'s
+    $text = str_replace("\t", "&nbsp;&nbsp;&nbsp;&nbsp;", $text);
+
+    // 3: Line Breaks
+    $text = preg_replace("/\r\n?|\n/", "<br>", $text);
+
+    // 4: Paragraphs (Replacing two or more line breaks with paragraph tags)
+    $text = preg_replace("/<br>\s*<br>/", "</p><p>", $text);
+
+    // 5: Wrap in Paragraph Tags
+    $text = "<p>" . $text . "</p>";
+
+    return $text;
+}
+
+function convertHtmlToText($html) {
+    // Replace <br>, <br/> and <br /> tags with newlines
+    $html = str_ireplace(["<br>", "<br/>", "<br />"], "\n", $html);
+
+    // Replace <p> and </p> tags with double newlines (to separate paragraphs)
+    $html = str_ireplace("</p>", "\n\n", $html);
+    $html = str_ireplace("<p>", "", $html); // Remove opening <p> tags
+
+    // Convert four &nbsp; into a tab (\t)
+    $html = str_ireplace("&nbsp;&nbsp;&nbsp;&nbsp;", "\t", $html);
+
+    // Replace other common HTML tags (like <div>, <span>, etc.) by stripping them
+    $html = strip_tags($html);
+
+    // Replace HTML entities like &amp;, &lt;, &gt;, &nbsp;, etc. with their corresponding characters
+    $html = html_entity_decode($html, ENT_QUOTES | ENT_HTML5, 'UTF-8');
+
+    // Remove excessive spaces and normalise newlines
+    $html = preg_replace('/[ ]+/', ' ', $html);  // Replace multiple spaces or tabs with a single space
+    $html = trim($html);  // Remove leading/trailing whitespace
+
+    return $html;
+}
+
+function formatColumn($str, $width) {
+    // Function to pad the strings to align the columns
+    return str_pad($str, $width, ' ', STR_PAD_RIGHT);
+}
+
+echo "[i] checking connections\n";
+$appInfo = getTriliumVersion($apiToken, $serverUrl);
+checkFolderAccess($localFolder);
+echo "[i] version: ".$appInfo['appVersion']."\n";
+
+echo "[i] gathering info\n";
+$structure = fetchAllNotes($parentNoteId, $apiToken, $serverUrl, $localFolder);
+$localFiles = scanLocalFiles($localFolder."Synced/", $localFolder."Synced/");
+mergeStructures($structure, $localFiles);
+compareFileHashes($structure);
+
+// Print the data tables
+printFolderTable($structure);
+printFileTable($structure);
+
+// syncing operations
+deleteItem($structure);
+createTriliumFolder($structure);
+createTriliumFile($structure);
+createDiskFileAndFolder($structure);
+updateExistingFile($structure);
+
+/*
+echo "####### DEBUG ##########\n";
+printFolderTable($structure);
+printFileTable($structure);
+*/