From 42130d675bc8e7a8f4ee82cb31bb9d2aa647cfff Mon Sep 17 00:00:00 2001 From: Mark Levison Date: Wed, 16 Jul 2025 13:56:18 -0400 Subject: [PATCH 01/10] Initial Checkin GenAITaskChecker --- mlevison.GenAITaskChecker/CHANGELOG.md | 27 ++++ mlevison.GenAITaskChecker/README.md | 41 ++++++ .../__tests__/NPPluginMain.NOTACTIVE.js | 124 ++++++++++++++++++ mlevison.GenAITaskChecker/plugin.json | 113 ++++++++++++++++ .../requiredFiles/html-plugin-comms.js | 106 +++++++++++++++ .../src/NPMessagesFromHTMLWindow.js | 67 ++++++++++ mlevison.GenAITaskChecker/src/NPPluginMain.js | 75 +++++++++++ .../src/NPTriggers-Hooks.js | 119 +++++++++++++++++ mlevison.GenAITaskChecker/src/index.js | 35 +++++ .../src/support/fetchOverrides.js | 57 ++++++++ .../google.search-for-something.json | 4 + .../src/support/helpers.js | 11 ++ 12 files changed, 779 insertions(+) create mode 100644 mlevison.GenAITaskChecker/CHANGELOG.md create mode 100644 mlevison.GenAITaskChecker/README.md create mode 100644 mlevison.GenAITaskChecker/__tests__/NPPluginMain.NOTACTIVE.js create mode 100644 mlevison.GenAITaskChecker/plugin.json create mode 100644 mlevison.GenAITaskChecker/requiredFiles/html-plugin-comms.js create mode 100644 mlevison.GenAITaskChecker/src/NPMessagesFromHTMLWindow.js create mode 100644 mlevison.GenAITaskChecker/src/NPPluginMain.js create mode 100644 mlevison.GenAITaskChecker/src/NPTriggers-Hooks.js create mode 100644 mlevison.GenAITaskChecker/src/index.js create mode 100644 mlevison.GenAITaskChecker/src/support/fetchOverrides.js create mode 100644 mlevison.GenAITaskChecker/src/support/fetchResponses/google.search-for-something.json create mode 100644 mlevison.GenAITaskChecker/src/support/helpers.js diff --git a/mlevison.GenAITaskChecker/CHANGELOG.md b/mlevison.GenAITaskChecker/CHANGELOG.md new file mode 100644 index 000000000..b200584b7 --- /dev/null +++ b/mlevison.GenAITaskChecker/CHANGELOG.md @@ -0,0 +1,27 @@ +# mlevison.GenAITaskChecker Changelog + +## About mlevison.GenAITaskChecker Plugin + +See Plugin [README](https://github.com/NotePlan/plugins/blob/main/mlevison.GenAITaskChecker/README.md) for details on available commands and use case. + +## [x.x.x] - yyyy-mm-dd (githubUserName) + +### Added +List what has been added. If nothing has been changed, this section can be removed. + +### Changed +List what has changed. If nothing has been changed, this section can be removed. + +### Removed +List what has removed. If nothing has been removed, this section can be removed. + +## Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## Plugin Versioning Uses Semver + +All NotePlan plugins follow `semver` versioning. For details, please refer to [semver website](https://semver.org/) diff --git a/mlevison.GenAITaskChecker/README.md b/mlevison.GenAITaskChecker/README.md new file mode 100644 index 000000000..13fe32a94 --- /dev/null +++ b/mlevison.GenAITaskChecker/README.md @@ -0,0 +1,41 @@ +# GenAITaskChecker Noteplan Plugin + +## Latest Updates + +See [CHANGELOG](https://github.com/NotePlan/plugins/blob/main/mlevison.GenAITaskChecker/CHANGELOG.md) for latest updates/changes to this plugin. + +## About This Plugin + +Finds all tasks in the current note and submits them the LLM asking if the tasks are aligned with quarterly plan. + +[You will delete this text and replace it with a readme about your plugin -- not ever seen by users, but good for people looking at your code. Before you delete though, you should know:] + +You do not need all of this scaffolding for a basic NP plugin. As the instructions state [Creating Plugins](https://help.noteplan.co/article/65-commandbar-plugins), you can create a plugin with just two files: `plugin.json` and `script.js`. Please read that whole page before proceeding here. + +However, for more complex plugins, you may find that it's easier to write code in multiple files, incorporating code (helper functions, etc.) written (and *TESTED*) previously by others. We strongly recommend type checking (e.g. [Flow.io](https://flow.io)) to help validate the code you write. If either of those is interesting to you, you're in the right place. Before going any further, make sure you follow the development environment [setup instructions](https://github.com/NotePlan/plugins). + +## Creating NotePlan Plugin + +You can create a NotePlan plugin by executing: + +```bash +noteplan-cli plugin:create +``` + +Open up a terminal folder and change directory to the plugins repository root. Run the command `npm run autowatch` which will keep looking for changes to all plugin files and will re-compile when JavaScript changes are made. It will also transpile ES6 and ES7 code to ES5 which will run on virtually all Macs, and will copy the file(s) to the NotePlan Plugins folder, so you can immediately test in Noteplan. + +### NotePlan Plugins Directory +You can find all your currently installed NotePlan Plugins here (for AppStore version of NotePlan): + +```bash +/Users//Library/Containers/co.noteplan.NotePlan3/Data/Library/Application Support/co.noteplan.NotePlan3/Plugins +``` + +Keep in mind that you can code/test without updating the plugin version property in `plugin.json`, however when you push the code to the Plugins repository (or create a PR), you should update the version number so that other NotePlan users who have installed your plugin will know that an updated version is available. + +Further to that point, you can use your plugin locally, or you can use `git` to create a Pull Request to get it merged in the Noteplan/plugins repository and potentially available for all users through the `NotePlan > Preferences > Plugins` tab. + +That's it. Happy coding! + +## NotePlan Plugin Team +Hat-tip to @eduard, @nmn, @jgclark, @dwertheimer and @codedungeon, who made all this fancy cool stuff. diff --git a/mlevison.GenAITaskChecker/__tests__/NPPluginMain.NOTACTIVE.js b/mlevison.GenAITaskChecker/__tests__/NPPluginMain.NOTACTIVE.js new file mode 100644 index 000000000..f0d7b79e3 --- /dev/null +++ b/mlevison.GenAITaskChecker/__tests__/NPPluginMain.NOTACTIVE.js @@ -0,0 +1,124 @@ +/* + * THIS FILE SHOULD BE RENAMED WITH ".test.js" AT THE END SO JEST WILL FIND AND RUN IT + * It is included here as an example/starting point for your own tests + */ + +/* global jest, describe, test, expect, beforeAll, afterAll, beforeEach, afterEach */ +// Jest testing docs: https://jestjs.io/docs/using-matchers +/* eslint-disable */ + +import * as f from '../src/sortTasks' +import { CustomConsole, LogType, LogMessage } from '@jest/console' // see note below +import { Calendar, Clipboard, CommandBar, DataStore, Editor, NotePlan, simpleFormatter /* Note, mockWasCalledWithString, Paragraph */ } from '@mocks/index' + +const PLUGIN_NAME = `{{pluginID}}` +const FILENAME = `NPPluginMain` + +beforeAll(() => { + global.Calendar = Calendar + global.Clipboard = Clipboard + global.CommandBar = CommandBar + global.DataStore = DataStore + global.Editor = Editor + global.NotePlan = NotePlan + global.console = new CustomConsole(process.stdout, process.stderr, simpleFormatter) // minimize log footprint + DataStore.settings['_logLevel'] = 'none' //change this to DEBUG to get more logging (or 'none' for none) +}) + +/* Samples: +expect(result).toMatch(/someString/) +expect(result).not.toMatch(/someString/) +expect(result).toEqual([]) +import { mockWasCalledWith } from '@mocks/mockHelpers' + const spy = jest.spyOn(console, 'log') + const result = mainFile.getConfig() + expect(mockWasCalledWith(spy, /config was empty/)).toBe(true) + spy.mockRestore() + + test('should return the command object', () => { + const result = f.getPluginCommands({ 'plugin.commands': [{ a: 'foo' }] }) + expect(result).toEqual([{ a: 'foo' }]) + }) +*/ + +describe('mlevison.GenAITaskChecker' /* pluginID */, () => { + describe('NPPluginMain' /* file */, () => { + describe('sayHello' /* function */, () => { + test('should insert text if called with a string param', async () => { + // tests start with "should" to describe the expected behavior + const spy = jest.spyOn(Editor, 'insertTextAtCursor') + const result = await mainFile.sayHello('Testing...') + expect(spy).toHaveBeenCalled() + expect(spy).toHaveBeenNthCalledWith( + 1, + `***You clicked the link!*** The message at the end of the link is "Testing...". Now the rest of the plugin will run just as before...\n\n`, + ) + spy.mockRestore() + }) + test('should write result to console', async () => { + // tests start with "should" to describe the expected behavior + const spy = jest.spyOn(console, 'log') + const result = await mainFile.sayHello() + expect(spy).toHaveBeenCalled() + expect(spy).toHaveBeenNthCalledWith(1, expect.stringMatching(/The plugin says: HELLO WORLD FROM TEST PLUGIN!/)) + spy.mockRestore() + }) + test('should call DataStore.settings', async () => { + // tests start with "should" to describe the expected behavior + const oldValue = DataStore.settings + DataStore.settings = { settingsString: 'settingTest' } + const spy = jest.spyOn(Editor, 'insertTextAtCursor') + const _ = await mainFile.sayHello() + expect(spy).toHaveBeenCalled() + expect(spy).toHaveBeenNthCalledWith(1, expect.stringMatching(/settingTest/)) + DataStore.settings = oldValue + spy.mockRestore() + }) + test('should call DataStore.settings if no value set', async () => { + // tests start with "should" to describe the expected behavior + const oldValue = DataStore.settings + DataStore.settings = { settingsString: undefined } + const spy = jest.spyOn(Editor, 'insertTextAtCursor') + const _ = await mainFile.sayHello() + expect(spy).toHaveBeenCalled() + expect(spy).toHaveBeenNthCalledWith(1, expect.stringMatching(/\*\*\"\"\*\*/)) + DataStore.settings = oldValue + spy.mockRestore() + }) + test('should CLO write note.paragraphs to console', async () => { + // tests start with "should" to describe the expected behavior + const prevEditorNoteValue = copyObject(Editor.note || {}) + Editor.note = new Note({ filename: 'testingFile' }) + Editor.note.paragraphs = [new Paragraph({ content: 'testingParagraph' })] + const spy = jest.spyOn(console, 'log') + const result = await mainFile.sayHello() + expect(spy).toHaveBeenCalled() + expect(spy).toHaveBeenNthCalledWith(2, expect.stringMatching(/\"content\": \"testingParagraph\"/)) + Editor.note = prevEditorNoteValue + spy.mockRestore() + }) + test('should insert a link if not called with a string param', async () => { + // tests start with "should" to describe the expected behavior + const spy = jest.spyOn(Editor, 'insertTextAtCursor') + const result = await mainFile.sayHello('') + expect(spy).toHaveBeenCalled() + expect(spy).toHaveBeenLastCalledWith(expect.stringMatching(/noteplan:\/\/x-callback-url\/runPlugin/)) + spy.mockRestore() + }) + test('should write an error to console on throw', async () => { + // tests start with "should" to describe the expected behavior + const spy = jest.spyOn(console, 'log') + const oldValue = Editor.insertTextAtCursor + delete Editor.insertTextAtCursor + try { + const result = await mainFile.sayHello() + } catch (e) { + expect(e.message).stringMatching(/ERROR/) + } + expect(spy).toHaveBeenCalledWith(expect.stringMatching(/ERROR/)) + Editor.insertTextAtCursor = oldValue + spy.mockRestore() + }) + }) + }) +}) diff --git a/mlevison.GenAITaskChecker/plugin.json b/mlevison.GenAITaskChecker/plugin.json new file mode 100644 index 000000000..3933fd7af --- /dev/null +++ b/mlevison.GenAITaskChecker/plugin.json @@ -0,0 +1,113 @@ +{ + "COMMENT": "Details on these fields: https://help.noteplan.co/article/67-create-command-bar-plugins", + "macOS.minVersion": "10.13.0", + "noteplan.minAppVersion": "3.4.0", + "plugin.id": "mlevison.GenAITaskChecker", + "plugin.name": "🧩 GenAITaskChecker", + "plugin.version": "0.1.0", + "plugin.lastUpdateInfo": "Describe this update", + "plugin.description": "Finds all tasks in the current note and submits them the LLM asking if the tasks are aligned with quarterly plan.", + "plugin.author": "Agile Pain Relief Consulting", + "plugin.requiredFiles-EDIT_ME": ["html-plugin-comms.js"], + "plugin.requiredFiles-NOTE": "If you want to use HTML windows, remove the '-EDIT_ME' ABOVE", + "plugin.dependsOn": [], + "plugin.script": "script.js", + "plugin.url": "https://github.com/NotePlan/plugins/blob/main/mlevison.GenAITaskChecker/README.md", + "plugin.changelog": "https://github.com/NotePlan/plugins/blob/main/mlevison.GenAITaskChecker/CHANGELOG.md", + "plugin.commands": [ + { + "note": "================== COMMMANDS ========================" + }, + { + "name": "Say Hello from mlevison.GenAITaskChecker! (change this to your own command)", + "description": "Your first plugin!", + "jsFunction": "sayHello", + "alias": [ + "helloWorld Alias" + ], + "arguments": [ + "Append this text to document" + ] + }, + { + "NOTE": "DO NOT EDIT THIS COMMAND/TRIGGER", + "name": "GenAITaskChecker: Version", + "description": "Update + Check Version", + "jsFunction": "versionCheck" + }, + { + "description": "DO NOT EDIT THIS COMMAND/TRIGGER", + "name": "onOpen", + "jsFunction": "onOpen", + "hidden": true + }, + { + "description": "DO NOT EDIT THIS COMMAND/TRIGGER", + "name": "onEditorWillSave", + "jsFunction": "onEditorWillSave", + "hidden": true + }, + { + "NOTE": "DO NOT EDIT THIS COMMAND/TRIGGER", + "name": "onMessageFromHTMLView", + "description": "mlevison.GenAITaskChecker: Callback function to receive messages from HTML view", + "jsFunction": "onMessageFromHTMLView", + "hidden": true + }, + { + "NOTE": "DO NOT EDIT THIS COMMAND/TRIGGER", + "name": "GenAITaskChecker: Update Plugin Settings", + "description": "Preferences", + "jsFunction": "editSettings" + } + ], + "plugin.settings": [ + { + "note": "================== SETTINGS ========================" + }, + { + "COMMENT": "Plugin settings documentation: https://help.noteplan.co/article/123-plugin-configuration", + "type": "heading", + "title": "GenAITaskChecker Settings" + }, + { + "type": "hidden", + "key": "pluginID", + "default": "mlevison.GenAITaskChecker", + "COMMENT": "This is for use by the editSettings helper function. PluginID must match the plugin.id in the top of this file" + }, + { + "title": "A string in prefs", + "key": "settingsString", + "type": "string", + "description": "Enter some string and see it change when the plugin is run", + "default": "This default setting was set in plugin preferences!" + }, + { + "note": "================== DEBUGGING SETTINGS ========================" + }, + { + "NOTE": "DO NOT CHANGE THE FOLLOWING SETTINGS; ADD YOUR SETTINGS ABOVE ^^^", + "type": "separator" + }, + { + "type": "heading", + "title": "Debugging" + }, + { + "key": "_logLevel", + "type": "string", + "title": "Log Level", + "choices": [ + "DEBUG", + "INFO", + "WARN", + "ERROR", + "none" + ], + "description": "Set how much logging output will be displayed when executing GenAITaskChecker commands in NotePlan Plugin Console Logs (NotePlan -> Help -> Plugin Console)\n\n - DEBUG: Show All Logs\n - INFO: Only Show Info, Warnings, and Errors\n - WARN: Only Show Errors or Warnings\n - ERROR: Only Show Errors\n - none: Don't show any logs", + "default": "INFO", + "required": true + } + ] +} \ No newline at end of file diff --git a/mlevison.GenAITaskChecker/requiredFiles/html-plugin-comms.js b/mlevison.GenAITaskChecker/requiredFiles/html-plugin-comms.js new file mode 100644 index 000000000..90754ed28 --- /dev/null +++ b/mlevison.GenAITaskChecker/requiredFiles/html-plugin-comms.js @@ -0,0 +1,106 @@ +/** + * html-PluginComms.js - HTML Window: process data to/from the plugin + * This file is loaded by the browser via + + + + * 3) onMessageFromPlugin() below will receive your data from the plugin and route it to the appropriate function + * 4) Call sendMessageToPlugin(type:string, data:any) from your HTML window to send data back to the plugin asynchronously + */ + +/* eslint-disable no-undef */ +/* eslint-disable no-unused-vars */ + +/** + * onMessageFromPlugin is a router where you route the data returned from the plugin + * the plugin will send a 'type' and 'data' object + * this function is just a switch/router. Based on the type, call a function to process the data. + * do not do any processing here, just call the function to do the processing + * @param {string} type + * @param {any} data + */ +function onMessageFromPlugin(type, data) { + switch (type) { + case 'updateDiv': + onUpdateDivReceived(data) + break + default: + console.log(`onMessageFromPlugin: unknown type: ${type}`) + showError(`onMessageFromPlugin: received unknown type: ${type}`) + // ...call other functions to process the data for other types of messages from the plugin + } +} + +/**************************************************************************************************************************** + * DATA PROCESSING FUNCTIONS FOR RETURNED DATA FROM THE PLUGIN + ****************************************************************************************************************************/ +// these are the functions called in the onMessageFromPlugin function above + +/** + * Plugin wants to replace a div with some HTML (or plain text if innerText is true) + * @param { { divID: string, html: string, innerText:boolean } } data + */ +function onUpdateDivReceived(data) { + const { divID, html, innerText } = data + console.log(`onUpdateDivReceived: divID: ${divID}, html: ${html}`) + replaceHTML(divID, html, innerText) +} + +/**************************************************************************************************************************** + * EVENT HANDLERS FOR THE HTML VIEW + ****************************************************************************************************************************/ +// These event handlers are called by the HTML view when the user clicks on something +// It's a good idea to have a separate function for each event handler so that you can easily see what's going on +// And have the receiving function on the plugin side named the same thing as the event handler +// So it's easy to match them all up +// You could call sendMessageToPlugin directly from the HTML onClick event handler, but I prefer to have a separate function +// so you can do error checking, logging, etc. + +/** + * Event handler for the 'click' event on the status icon + * @param {string} filename + * @param {number} lineIndex + * @param {string} statusWas + */ +function onClickStatus(filename, lineIndex, statusWas, lineID) { + if (!filename || typeof lineIndex !== 'number' || !statusWas || !lineID) { + const msg = `onClickStatus: invalid data: filename: ${filename}, lineIndex: ${lineIndex}, statusWas: ${statusWas}, lineID: ${lineID}` + console.log(msg) + showError(msg) // you should have a div with id='error' in your HTML + } else { + console.log(`onClickStatus received click on: filename: ${filename}, lineIndex: ${lineIndex}, status: ${status}; sending 'onClickStatus' to plugin`) + const data = { filename, lineIndex: lineIndex, statusWas, lineID } + sendMessageToPlugin('onClickStatus', data) // actionName, data + } +} + +/**************************************************************************************************************************** + * HELPER FUNCTIONS + ****************************************************************************************************************************/ + +function replaceHTML(divID, html, innerText) { + console.log(`replaceHTML: divID: ${divID}, html: ${html}`) + const div = document.getElementById(divID) + if (div) { + if (innerText) { + div.innerText = html + } else { + div.innerHTML = html + } + } +} + +function showError(message) { + const div = document.getElementById('error') + if (div) { + div.innerText = message + } +} diff --git a/mlevison.GenAITaskChecker/src/NPMessagesFromHTMLWindow.js b/mlevison.GenAITaskChecker/src/NPMessagesFromHTMLWindow.js new file mode 100644 index 000000000..2f4739ab8 --- /dev/null +++ b/mlevison.GenAITaskChecker/src/NPMessagesFromHTMLWindow.js @@ -0,0 +1,67 @@ +// @flow +/** + * This file receives and processes messages from the HTML view + * You can ignore it if you are not going to use any HTML popup windows + * If you do want to use HTML windows, read the notes at the top of _requiredFiles/html-plugin-comms.js + * + * The function onClickStatus below is just an example of a function that could be called from the HTML view + */ + +import pluginJson from '../plugin.json' + +// ID needs match the custom id you pass when you open the window +const WINDOW_CUSTOM_ID = `${pluginJson['plugin.id']} HTML Window` // change if you want to use multiple html windows + +// import { getWindowIdFromCustomId } from '@helpers/NPWindows' +import { sendToHTMLWindow } from '@helpers/HTMLView' +import { getParagraphFromStaticObject } from '@helpers/NPParagraph' +import { log, logError, logDebug, timer, clo, JSP } from '@helpers/dev' + +/** + * Callback function to receive async messages from HTML view + * Plugin entrypoint for command: "/onMessageFromHTMLView" (called by plugin via sendMessageToHTMLView command) + * Do not do the processing in this function, but call a separate function to do the work. + * @author @dwertheimer + * @param {string} type - the type of action the HTML view wants the plugin to perform + * @param {any} data - the data that the HTML view sent to the plugin + */ +export function onMessageFromHTMLView(type: string, data: any): any { + try { + logDebug(pluginJson, `onMessageFromHTMLView running with args:${JSP(data)}`) + switch (type) { + case 'onClickStatus': + onClickStatus(data) // data is an array and could be multiple items. but in this case, we know we only need the first item which is an object + break + } + return {} // any function called by invoke... should return something (anything) to keep NP from reporting an error in the console + } catch (error) { + logError(pluginJson, JSP(error)) + } +} + +type ClickStatus = { filename: string, lineIndex: number, statusWas: string, lineID: string } + +/** + * Somebody clicked on a status icon in the HTML view + * (this is just a sample function called by the router - onMessageFromHTMLView() above) + * @param {ClickStatus} data - details of the item clicked + */ +export function onClickStatus(data: ClickStatus) { + const { filename, lineIndex, statusWas, lineID } = data + logDebug(pluginJson, `Plugin: onClickStatus running with statusWas:${statusWas}, filename:${filename}, lineIndex:${lineIndex}, statusWas:${statusWas}`) + const para = getParagraphFromStaticObject(data, ['filename', 'lineIndex']) + if (para) { + // you can do whatever you want here. For example, you could change the status of the paragraph + // to done depending on whether it was an open task or a checklist item + para.type = statusWas === 'open' ? 'done' : 'checklistDone' + para.note?.updateParagraph(para) + const newDivContent = `"${para.type}"Paragraph status was updated by the plugin!` + if (WINDOW_CUSTOM_ID) { + sendToHTMLWindow(WINDOW_CUSTOM_ID, 'updateDiv', { divID: lineID, html: newDivContent, innerText: false }) + } + // NOTE: in this particular case, it might have been easier to just call the refresh-page command, but I thought it worthwhile + // to show how to update a single div in the HTML view + } else { + logError(pluginJson, `onClickStatus: could not find paragraph for filename:${filename}, lineIndex:${lineIndex}`) + } +} diff --git a/mlevison.GenAITaskChecker/src/NPPluginMain.js b/mlevison.GenAITaskChecker/src/NPPluginMain.js new file mode 100644 index 000000000..7ed5ba5bf --- /dev/null +++ b/mlevison.GenAITaskChecker/src/NPPluginMain.js @@ -0,0 +1,75 @@ +// @flow +// Plugin code goes in files like this. Can be one per command, or several in a file. +// `export async function [name of jsFunction called by Noteplan]` +// then include that function name as an export in the index.js file also +// About Flow: https://flow.org/en/docs/usage/#toc-write-flow-code +// Getting started with Flow in NotePlan: https://github.com/NotePlan/plugins/blob/main/Flow_Guide.md + +// NOTE: This file is named NPPluginMain.js (you could change that name and change the reference to it in index.js) +// As a matter of convention, we use NP at the beginning of files which contain calls to NotePlan APIs (Editor, DataStore, etc.) +// Because you cannot easily write tests for code that calls NotePlan APIs, we try to keep the code in the NP files as lean as possible +// and put the majority of the work in the /support folder files which have Jest tests for each function +// support/helpers is an example of a testable file that is used by the plugin command +// REMINDER, to build this plugin as you work on it: +// From the command line: +// `noteplan-cli plugin:dev mlevison.GenAITaskChecker --test --watch --coverage` +// IMPORTANT: It's a good idea for you to open the settings ASAP in NotePlan Preferences > Plugins and set your plugin's logging level to DEBUG + +/** + * LOGGING + * A user will be able to set their logging level in the plugin's settings (if you used the plugin:create command) + * As a general rule, you should use logDebug (see below) for messages while you're developing. As developer, + * you will set your log level in your plugin preferences to DEBUG and you will see these messages but + * an ordinary user will not. When you want to output a message,you can use the following. + * logging level commands for different levels of messages: + * + * logDebug(pluginJson,"Only developers or people helping debug will see these messages") + * log(pluginJson,"Ordinary users will see these informational messages") + * logWarn(pluginJson,"All users will see these warning/non-fatal messages") + * logError(pluginJson,"All users will see these fatal/error messages") + */ +import pluginJson from '../plugin.json' +import * as helpers from './support/helpers' +import { log, logDebug, logError, logWarn, clo, JSP } from '@helpers/dev' +import { createRunPluginCallbackUrl } from '@helpers/general' + +// NOTE: Plugin entrypoints (jsFunctions called by NotePlan) must be exported as async functions or you will get a TypeError in the NotePlan plugin console +// if you do not have an "await" statement inside your function, you can put an eslint-disable line like below so you don't get an error +// eslint-disable-next-line require-await +export async function sayHello(incoming: ?string = ''): Promise { + // every command/plugin entry point should always be wrapped in a try/catch block + try { + if (incoming?.length) { + // When commands are launched from NotePlan Command Bar, they are passed with no arguments + // if `incoming` is set, this plugin/command run must have come from a runPlugin call (e.g. clicking on a noteplan:// xcallback link or a template call) + Editor.insertTextAtCursor(`***You clicked the link!*** The message at the end of the link is "${incoming}". Now the rest of the plugin will run just as before...\n\n`) + } + + // a call to a support function in a separate file + const message = helpers.uppercase('Hello World from Test Plugin!') + + // this will appear in NotePlan Plugin Console (NotePlan > Help > Plugin Console) + log(pluginJson, `The plugin says: ${message}`) + + // Get some info from the plugin settings panel (where users can change settings) + const settings = DataStore.settings // Plugin settings documentation: https://help.noteplan.co/article/123-plugin-configuration + const settingsString = settings.settingsString ?? '' + // this will be inserted at cursor position in the Editor + Editor.insertTextAtCursor( + `${message}\n\nThis came from the Plugin Settings Panel: **"${settingsString}"** (You should go now to Preferences > Plugins, click the "cog" next to this plugin name and change the text. When you run this plugin again, you will see the new setting text.\n\n`, + ) + + // This will Console Log an Object that comes from the NotePlan API (in this case, the currently-open Note's paragraphs) + clo(Editor.note?.paragraphs, `The note paragraphs:`) + + if (!incoming?.length) { + // Create a XCallback URL that can run this command + const url = createRunPluginCallbackUrl(pluginJson['plugin.id'], pluginJson['plugin.commands'][0].name, ['This text was in the link!']) + Editor.insertTextAtCursor( + `This link could be used anywhere inside or outside of NotePlan to call this plugin:\n${url}\nGo ahead and click it! ^^^\nYou will see the results below:\n\n*****\n`, + ) + } + } catch (error) { + logError(pluginJson, JSP(error)) + } +} diff --git a/mlevison.GenAITaskChecker/src/NPTriggers-Hooks.js b/mlevison.GenAITaskChecker/src/NPTriggers-Hooks.js new file mode 100644 index 000000000..56f6defc7 --- /dev/null +++ b/mlevison.GenAITaskChecker/src/NPTriggers-Hooks.js @@ -0,0 +1,119 @@ +/* eslint-disable require-await */ +// @flow + +import pluginJson from '../plugin.json' // gives you access to the contents of plugin.json +import { log, logError, logDebug, timer, clo, JSP } from '@helpers/dev' +import { updateSettingData, pluginUpdated } from '@helpers/NPConfiguration' +import { showMessage } from '@helpers/userInput' + +/** + * NOTEPLAN PER-NOTE TRIGGERS + * + * The following functions are called by NotePlan automatically + * if a note has a triggers: section in its frontmatter + * See the documentation: https://help.noteplan.co/article/173-plugin-note-triggers + */ + +/** + * onOpen + * Plugin entrypoint for command: "/onOpen" + * Called when a note is opened and that note + * has a triggers: onOpen in its frontmatter + * @param {TNote} note - current note in Editor + */ +export async function onOpen(note: TNote): Promise { + try { + logDebug(pluginJson, `${pluginJson['plugin.id']} :: onOpen running for note:"${String(note.filename)}"`) + // Try to guard against infinite loops of opens/refreshing + // You can delete this code if you are sure that your onOpen trigger will not cause an infinite loop + // But the safest thing to do is put your code inside the if loop below to ensure it runs no more than once every 15s + const now = new Date() + if (Editor?.note?.changedDate) { + const lastEdit = new Date(Editor?.note?.changedDate) + if (now.getTime() - lastEdit.getTime() > 15000) { + logDebug(pluginJson, `onOpen ${timer(lastEdit)} since last edit`) + // Put your code here or call a function that does the work + } else { + logDebug(pluginJson, `onOpen: Only ${timer(lastEdit)} since last edit (hasn't been 15s)`) + } + } + } catch (error) { + logError(pluginJson, `onOpen: ${JSP(error)}`) + } +} + +/** + * onEditorWillSave + * Plugin entrypoint for command: "/onEditorWillSave" + */ +export async function onEditorWillSave() { + try { + logDebug(pluginJson, `${pluginJson['plugin.id']} :: onEditorWillSave running with note in Editor:"${String(Editor.filename)}"`) + // Put your code here or call a function that does the work + // Note: as stated in the documentation, if you want to change any content in the Editor + // before the file is written, you should NOT use the *note* variable here to change content + // Instead, use Editor.* commands (e.g. Editor.insertTextAtCursor()) or Editor.updateParagraphs() + } catch (error) { + logError(pluginJson, `onEditorWillSave: ${JSP(error)}`) + } +} + +/* + * NOTEPLAN GLOBAL PLUGIN HOOKS + * + * The rest of these functions are called by NotePlan automatically under certain conditions + * It is unlikely you will need to edit/add anything below this line + * + */ + +/** + * NotePlan calls this function after the plugin is installed or updated. + * The `updateSettingData` function looks through the new plugin settings in plugin.json and updates + * the user preferences to include any new fields + */ +export async function onUpdateOrInstall(): Promise { + try { + logDebug(pluginJson, `${pluginJson['plugin.id']} :: onUpdateOrInstall running`) + await updateSettingData(pluginJson) + await pluginUpdated(pluginJson, { code: 2, message: `Plugin Installed.` }) + } catch (error) { + logError(pluginJson, `onUpdateOrInstall: ${JSP(error)}`) + } +} + +/** + * NotePlan calls this function every time the plugin is run (any command in this plugin, including triggers) + * You should not need to edit this function. All work should be done in the commands themselves + */ +export function init(): void { + try { + logDebug(pluginJson, `${pluginJson['plugin.id']} :: init running`) + // clo(DataStore.settings, `${pluginJson['plugin.id']} Plugin Settings`) + DataStore.installOrUpdatePluginsByID([pluginJson['plugin.id']], false, false, false).then((r) => pluginUpdated(pluginJson, r)) + } catch (error) { + logError(pluginJson, `init: ${JSP(error)}`) + } +} + +/** + * NotePlan calls this function settings are updated in the Preferences panel + * You should not need to edit this function + */ +export async function onSettingsUpdated(): Promise { + try { + logDebug(pluginJson, `${pluginJson['plugin.id']} :: onSettingsUpdated running`) + } catch (error) { + logError(pluginJson, `onSettingsUpdated: ${JSP(error)}`) + } +} + +/** + * Check the version of the plugin (and force an update if the version is out of date) + */ +export async function versionCheck(): Promise { + try { + await showMessage(`Current Version: ${pluginJson['plugin.version']}`, 'OK', `${pluginJson['plugin.name']}`, true) + } catch (error) { + logError(pluginJson, JSP(error)) + } +} diff --git a/mlevison.GenAITaskChecker/src/index.js b/mlevison.GenAITaskChecker/src/index.js new file mode 100644 index 000000000..baeed8c43 --- /dev/null +++ b/mlevison.GenAITaskChecker/src/index.js @@ -0,0 +1,35 @@ +// @flow +// Flow typing is important for reducing errors and improving the quality of the code. +// About Flow: https://flow.org/en/docs/usage/#toc-write-flow-code +// Getting started with Flow in NotePlan: https://github.com/NotePlan/plugins/blob/main/Flow_Guide.md +// Note: As you will see in this plugin folder, you can have multiple files -- e.g. one file per command or logical group of commands +// ...and separate files for helper/support functions that can be tested in isolation +// The `autowatch` packager combines them all into one script.js file for NotePlan to read +// From the command line: +// `noteplan-cli plugin:dev {{pluginId}} --test --watch --coverage` +// ...will watch for changes and will compile the Plugin script code +// and copy it to your plugins directory where NotePlan can find it +// Since NP reloads the Javascript every time you CMD-J to insert a plugin, +// you can immediately test the new code without restarting NotePlan +// This index.js file is where the packager starts looking for files to combine into one script.js file +// So you need to add a line below for each function that you want NP to have access to. +// Typically, listed below are only the top-level plug-in functions listed in plugin.json + +export { sayHello } from './NPPluginMain' // add one of these for every command specifified in plugin.json (the function could be in any file as long as it's exported) + +// FETCH mocking for offline testing +// If you want to use external server calls in your plugin, it can be useful to mock the server responses +// while you are developing the plugin. This allows you to test the plugin without having to +// have a server running or having to have a network connection (or wait/pay for the server calls) +// Comment the following import line out if you want to use live fetch/server endpoints (normal operation) +// Uncomment it for using server mocks (fake/canned responses) you define in support/fetchOverrides.js +// import './support/fetchOverrides' + +/** + * Other imports/exports - you will normally not need to edit these + */ +// eslint-disable-next-line import/order +export { editSettings } from '@helpers/NPSettings' +export { onUpdateOrInstall, init, onSettingsUpdated, versionCheck } from './NPTriggers-Hooks' +export { onOpen, onEditorWillSave } from './NPTriggers-Hooks' +export { onMessageFromHTMLView } from './NPMessagesFromHTMLWindow' diff --git a/mlevison.GenAITaskChecker/src/support/fetchOverrides.js b/mlevison.GenAITaskChecker/src/support/fetchOverrides.js new file mode 100644 index 000000000..b0dd0bf57 --- /dev/null +++ b/mlevison.GenAITaskChecker/src/support/fetchOverrides.js @@ -0,0 +1,57 @@ +// @flow + +// This file is only loaded and fetch is overridden if the import is enabled in the index file + +/** + * FETCH MOCKING + * This file is used to override the fetch function (calls to an external server) with a fake response + * This allows you to test your plugin without having to have a server running or having to have a network connection + * or wait/pay for the server calls + * You can define your fake responses in this file or in a separate file (see below) + * ...and when your code makes a fetch call to a server, it will get (an appropriate) fake response instead + * You define the responses and the text that must be in the fetch call to yield a particular response + * (see the mockResponses array below) + */ + +/** + * 1) Import any of your fake responses that are saved as files here (or see below for defining them as strings) + * The file should contain the exact response that the live server would return + * You can save the response as a JSON file (like sampleFileResponse below) or as a string (like sampleTextResponse below) + */ +import sampleFileResponse from './fetchResponses/google.search-for-something.json' // a sample fake response saved as a JSON file + +// Other necessary imports +import { FetchMock, type FetchMockResponse } from '@mocks/Fetch.mock' +import { logDebug } from '@helpers/dev' + +/** + * 2) Or you can define your fake responses as strings in this file: + */ +// You could also just put all the fake responses here in this file +// A little messier, but if you don't have very many responses, or they are small/strings, it's fine +const sampleTextWeatherResponse = `Nuremberg: ☀️ +9°F` + +// 3) So the mock knows when to send back which response, you need to define the match and response for each mock response +// Fill in the match and response for each mock response you want to use +// The match object hast following properties: +// url: string - the url to match (can be a partial string, or can even be a string that includes regex) +// optionsBody: string - a partial string or string/regex included in the POST body of the request to match (sent in options.body to fetch) +// optionsBody is optional. If you don't need to match on the POST body (matching URL is enough), just leave it out +// The response MUST BE A STRING. So either use a string response (like sampleTextWeatherResponse above) or +// JSON.stringify your response object (like sampleFileResponse above) +const mockResponses: Array = [ + // the first mock below will match a POST request to google.com with the words "search for something" in the POST body + { match: { url: 'google.com', optionsBody: 'search for something' }, response: JSON.stringify(sampleFileResponse) }, + // the mock below will match any GET or POST request to "wttr.in/Nuremberg?format=3" regardless of the body + { match: { url: 'wttr.in/Nuremberg?format=3' }, response: sampleTextWeatherResponse }, +] + +/** + * DO NOT TOUCH ANYTHING BELOW THIS LINE + */ + +const fm = new FetchMock(mockResponses) // add one object to array for each mock response +fetch = async (url: string, opts: FetchOptions) => { + logDebug(`fetchOverrides.js`, `FetchMock faking response from: "${url}" (turn on/off in index.js)`) + return await fm.fetch(url, opts) +} //override the global fetch diff --git a/mlevison.GenAITaskChecker/src/support/fetchResponses/google.search-for-something.json b/mlevison.GenAITaskChecker/src/support/fetchResponses/google.search-for-something.json new file mode 100644 index 000000000..988c28a6c --- /dev/null +++ b/mlevison.GenAITaskChecker/src/support/fetchResponses/google.search-for-something.json @@ -0,0 +1,4 @@ +{ + "someKey": "Some Value", + "youGet": "The Idea" +} \ No newline at end of file diff --git a/mlevison.GenAITaskChecker/src/support/helpers.js b/mlevison.GenAITaskChecker/src/support/helpers.js new file mode 100644 index 000000000..ca321f04c --- /dev/null +++ b/mlevison.GenAITaskChecker/src/support/helpers.js @@ -0,0 +1,11 @@ +// @flow +// Here's an example function that can be imported and used in the plugin code +// More importantly, this function is pure (no NotePlan API calls), which means it can be tested +// This is a good way to do much of your plugin work in isolation, with tests, and then the NPxxx files can be smaller +// And just focus on NotePlan input/output, with the majority of the work happening here +// Reminder: +// About Flow: https://flow.org/en/docs/usage/#toc-write-flow-code +// Getting started with Flow in NotePlan: https://github.com/NotePlan/plugins/blob/main/Flow_Guide.md +export function uppercase(str: string = ''): string { + return str.toUpperCase() +} From a729394681cdfe8c3b40543099dea51c552215db Mon Sep 17 00:00:00 2001 From: Mark Levison Date: Sun, 20 Jul 2025 15:37:35 -0400 Subject: [PATCH 02/10] Plugin Initialized and First Test Case working --- ...ACTIVE.js => NPPluginMain.NOTACTIVE.notjs} | 2 +- .../extractAllTasksFromCurrentNote.test.js | 46 ++++++++++++ .../sample files/TestNoteWithTasks.md | 71 +++++++++++++++++++ 3 files changed, 118 insertions(+), 1 deletion(-) rename mlevison.GenAITaskChecker/__tests__/{NPPluginMain.NOTACTIVE.js => NPPluginMain.NOTACTIVE.notjs} (99%) create mode 100644 mlevison.GenAITaskChecker/__tests__/extractAllTasksFromCurrentNote.test.js create mode 100644 mlevison.GenAITaskChecker/__tests__/sample files/TestNoteWithTasks.md diff --git a/mlevison.GenAITaskChecker/__tests__/NPPluginMain.NOTACTIVE.js b/mlevison.GenAITaskChecker/__tests__/NPPluginMain.NOTACTIVE.notjs similarity index 99% rename from mlevison.GenAITaskChecker/__tests__/NPPluginMain.NOTACTIVE.js rename to mlevison.GenAITaskChecker/__tests__/NPPluginMain.NOTACTIVE.notjs index f0d7b79e3..657fe3098 100644 --- a/mlevison.GenAITaskChecker/__tests__/NPPluginMain.NOTACTIVE.js +++ b/mlevison.GenAITaskChecker/__tests__/NPPluginMain.NOTACTIVE.notjs @@ -7,7 +7,7 @@ // Jest testing docs: https://jestjs.io/docs/using-matchers /* eslint-disable */ -import * as f from '../src/sortTasks' + import { CustomConsole, LogType, LogMessage } from '@jest/console' // see note below import { Calendar, Clipboard, CommandBar, DataStore, Editor, NotePlan, simpleFormatter /* Note, mockWasCalledWithString, Paragraph */ } from '@mocks/index' diff --git a/mlevison.GenAITaskChecker/__tests__/extractAllTasksFromCurrentNote.test.js b/mlevison.GenAITaskChecker/__tests__/extractAllTasksFromCurrentNote.test.js new file mode 100644 index 000000000..eabd9c793 --- /dev/null +++ b/mlevison.GenAITaskChecker/__tests__/extractAllTasksFromCurrentNote.test.js @@ -0,0 +1,46 @@ +/* global jest, describe, test, expect, beforeAll, afterAll, beforeEach, afterEach */ +import { _ } from 'lodash' +import * as sorting from '../../helpers/sorting' +import { CustomConsole, LogType, LogMessage } from '@jest/console' // see note below +import { Calendar, Clipboard, CommandBar, DataStore, Editor, NotePlan, simpleFormatter, Paragraph /* Note, mockWasCalledWithString, Paragraph */ } from '@mocks/index' + + +beforeAll(() => { + global.Calendar = Calendar + global.Clipboard = Clipboard + global.CommandBar = CommandBar + global.DataStore = DataStore + global.Editor = Editor + global.NotePlan = NotePlan + global.console = new CustomConsole(process.stdout, process.stderr, simpleFormatter) // minimize log footprint + DataStore.settings['_logLevel'] = 'DEBUG' //change this to DEBUG to get more logging (or 'none' for none) +}) + +describe('mlevison.GenAITaskChecker', () => { + describe('getAllTasks', () => { + test('prove All Tasks found in sample note', () => { + const noteA = { + paragraphs: [ + { type: 'title', lineIndex: 0, content: 'NoteA Title', headingLevel: 1 }, + { type: 'empty', lineIndex: 1 }, + { type: 'title', lineIndex: 2, content: 'Tasks for 3.4.22', headingLevel: 2 }, + { type: 'open', lineIndex: 3, content: 'task 1' }, + { type: 'title', lineIndex: 4, content: 'Journal for 3.4.22' }, + { type: 'list', lineIndex: 5, content: 'first journal entry' }, + { type: 'list', lineIndex: 6, content: 'second journal entry' }, + { type: 'empty', lineIndex: 7 }, + { type: 'title', lineIndex: 8, content: 'Done ...', headingLevel: 2 }, + { type: 'title', lineIndex: 9, content: 'Cancelled', headingLevel: 2 }, + { type: 'cancelled', lineIndex: 10, content: 'task 4 not done' }, + ], + } + const foundTasks = sorting.getTasksByType(noteA.paragraphs) + + console.log('foundTasks open', foundTasks.open, foundTasks.done, foundTasks.cancelled) + + expect(foundTasks.open.length).toBe(1) + expect(foundTasks.done.length).toBe(0) + expect(foundTasks.cancelled.length).toBe(1) + }) + }) +}) \ No newline at end of file diff --git a/mlevison.GenAITaskChecker/__tests__/sample files/TestNoteWithTasks.md b/mlevison.GenAITaskChecker/__tests__/sample files/TestNoteWithTasks.md new file mode 100644 index 000000000..2f592c061 --- /dev/null +++ b/mlevison.GenAITaskChecker/__tests__/sample files/TestNoteWithTasks.md @@ -0,0 +1,71 @@ +# Theme Test Note (for @jgclark themes) +# H1 colour and size +## H2 colour and size +### H3 colour and size +#### H4 colour and size +### Other things that are part of standard themes +With both **bold** and _italic_ text, and a ***combination***. +From v3.9.4 we can now have ==some nicer highlighting== or ~~strikethrough~~ around things. Even ~underlining~ _if we must_. +- #test #tags and @mentions are highlighted (very slightly differently) and have their links turned off +- here's `some inline code` and body text +* ! do +* !! something +* !!! very important +* >> working on something +* [>] ! Important Todo in the future >2021-08-04 +* [>] Scheduled into the future >2031-03-02 +* [x] Completed todo with #tagging and @ztag/subtag(test) @done(2021-02-13) +* [-] Cancelled todo with [How to be Generous to a Friend With Depression](https://www.stewardship.org.uk/blog/blog/post/733-how-to-be-generous-to-a-friend-with-depression?utm_source=Stewardship) ++ Checklist open ++ [>] Checklist scheduled and with a sync marker ^7qfqph ++ [x] Checklist done ++ [-] Checklist cancelled +> Every disease that submits to a cure shall be cured: but we will not call blue yellow to please those who insist on still having jaundice, nor make a midden of the world’s garden for the sake of some who cannot abide the smell of roses.’ -- C. S. Lewis (The Great Divorce) +- Bullets go like this + - and then this +Frontmatter like fields: look like this. +--- +### Markdown Links +The markdown syntax in Markdown links gets hidden, but appear when cursor is in the link: [NotePlan website](https://noteplan.co/). +### Note Links +- Standard Note links are shown without the surrounding brackets: [[Blank Header TEST]] +- Arrow note links are shown as above, but with a slightly different background colour. e.g. >Blank Header TEST<. They avoid the back-reference showing in that page's References section. (See also [How to create a link without a backlink?](https://help.noteplan.co/article/143-how-to-create-a-link-without-a-backlink#arrowlinks)) +### Time Blocks +For people using the AutoTimeBlocking plugin with the default settings, this hides the tag `#🕑` which `/atb` includes on lines it creates: #🕑 +### Comments +You can hide comments until you move your cursor into them, either inline like this: %%something in the middle%%, or at the end of a line like this: // something at the end +### Special highlight +Some people have a special use for a highlight that runs until the end of the line. To do this insert a Pilcrow (Alt-7) ¶ and see the effect. +### Fields and attributes +Fields: also supported, which might be useful for front matter, until fuller support for theming that emerges. +- Note: fields only apply at the start of a text line (not header, bullet etc.) +Attributes:: also can be highlighted at the start of a line. +### Tags in Template definitions +Here's a active tag: <% template tag %> and one that is commented out: <%# commented out template tag %> . +### Code Block +```markdown +## Heading +Test code block :+\/ +01234567890 ABCEDFGHijklmnopqrstu *with* **emphasis** and `things`. +1. list +* bullet +``` +--- +### More info +- [Customize Themes - NotePlan Knowledge Base](https://help.noteplan.co/article/44-customize-themes) +- [Theme Changelog - NotePlan Knowledge Base](https://help.noteplan.co/article/211-theme-changelog) +- My repo is [GitHub - jgclark/NP-themes: My NotePlan themes](https://github.com/jgclark/NP-themes) +- Brokosz has a repo at [GitHub - brokosz/NotePlan_Themes: Small collection of custom themes for NotePlan 3](https://github.com/brokosz/NotePlan_Themes) + +--- +## Full para breaks +Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore. + +Et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. + +Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. +## Single newline breaks +Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore. +Et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. +Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. +![📅](2023-10-26 14:00:::8A2C2207-98C9-40C1-9489-242CFFF95435:::NA:::1CB Team #meeting:::#1BADF8) From 9cf6d954e36c9302ebfe2ebd8b19ee0bb5db3302 Mon Sep 17 00:00:00 2001 From: Mark Levison Date: Sun, 20 Jul 2025 17:09:35 -0400 Subject: [PATCH 03/10] Add limited version of getAllTasksForCurrentNote --- .../extractAllTasksFromCurrentNote.test.js | 68 +++++++++++++++++-- .../src/support/{helpers.js => tasks.js} | 17 +++-- 2 files changed, 73 insertions(+), 12 deletions(-) rename mlevison.GenAITaskChecker/src/support/{helpers.js => tasks.js} (50%) diff --git a/mlevison.GenAITaskChecker/__tests__/extractAllTasksFromCurrentNote.test.js b/mlevison.GenAITaskChecker/__tests__/extractAllTasksFromCurrentNote.test.js index eabd9c793..12f0cd344 100644 --- a/mlevison.GenAITaskChecker/__tests__/extractAllTasksFromCurrentNote.test.js +++ b/mlevison.GenAITaskChecker/__tests__/extractAllTasksFromCurrentNote.test.js @@ -1,6 +1,7 @@ /* global jest, describe, test, expect, beforeAll, afterAll, beforeEach, afterEach */ import { _ } from 'lodash' -import * as sorting from '../../helpers/sorting' +import { getAllTasksFromCurrentNote } from '../src/support/tasks.js' +import * as sorting from '@helpers/sorting' import { CustomConsole, LogType, LogMessage } from '@jest/console' // see note below import { Calendar, Clipboard, CommandBar, DataStore, Editor, NotePlan, simpleFormatter, Paragraph /* Note, mockWasCalledWithString, Paragraph */ } from '@mocks/index' @@ -13,15 +14,15 @@ beforeAll(() => { global.Editor = Editor global.NotePlan = NotePlan global.console = new CustomConsole(process.stdout, process.stderr, simpleFormatter) // minimize log footprint - DataStore.settings['_logLevel'] = 'DEBUG' //change this to DEBUG to get more logging (or 'none' for none) + DataStore.settings['_logLevel'] = 'none' //change this to DEBUG to get more logging (or 'none' for none) }) describe('mlevison.GenAITaskChecker', () => { - describe('getAllTasks', () => { - test('prove All Tasks found in sample note', () => { - const noteA = { + describe('getAllTasks using getTasksByType', () => { + test('prove Open and Cancelled Tasks found in sample note', () => { + const noteWIthOpenAndCancelledTasks = { paragraphs: [ - { type: 'title', lineIndex: 0, content: 'NoteA Title', headingLevel: 1 }, + { type: 'title', lineIndex: 0, content: 'Title', headingLevel: 1 }, { type: 'empty', lineIndex: 1 }, { type: 'title', lineIndex: 2, content: 'Tasks for 3.4.22', headingLevel: 2 }, { type: 'open', lineIndex: 3, content: 'task 1' }, @@ -34,13 +35,66 @@ describe('mlevison.GenAITaskChecker', () => { { type: 'cancelled', lineIndex: 10, content: 'task 4 not done' }, ], } - const foundTasks = sorting.getTasksByType(noteA.paragraphs) + const foundTasks = sorting.getTasksByType(noteWIthOpenAndCancelledTasks.paragraphs) console.log('foundTasks open', foundTasks.open, foundTasks.done, foundTasks.cancelled) expect(foundTasks.open.length).toBe(1) expect(foundTasks.done.length).toBe(0) expect(foundTasks.cancelled.length).toBe(1) + expect(foundTasks.scheduled.length).toBe(0) + expect(foundTasks.checklist.length).toBe(0) + expect(foundTasks.checklistDone.length).toBe(0) + expect(foundTasks.checklistCancelled.length).toBe(0) + expect(foundTasks.checklistScheduled.length).toBe(0) }) + + test('prove Done Tasks found in sample note', () => { + const noteWIthDoneAndCancelledTasks = { + paragraphs: [ + { type: 'title', lineIndex: 0, content: 'Title', headingLevel: 1 }, + { type: 'empty', lineIndex: 1 }, + { type: 'title', lineIndex: 2, content: 'Tasks for 3.4.22', headingLevel: 2 }, + { type: 'scheduled', lineIndex: 3, content: 'task 1' }, + { type: 'title', lineIndex: 4, content: 'Journal for 3.4.22' }, + { type: 'list', lineIndex: 5, content: 'first journal entry' }, + { type: 'list', lineIndex: 6, content: 'second journal entry' }, + { type: 'empty', lineIndex: 7 }, + { type: 'title', lineIndex: 8, content: 'Done ...', headingLevel: 2 }, + { type: 'done', lineIndex: 10, content: 'task finised' }, + ] + } + const foundTasks = sorting.getTasksByType(noteWIthDoneAndCancelledTasks.paragraphs) + + console.log('foundTasks open', foundTasks.open, foundTasks.done, foundTasks.cancelled) + + expect(foundTasks.open.length).toBe(0) + expect(foundTasks.done.length).toBe(1) + expect(foundTasks.cancelled.length).toBe(0) + expect(foundTasks.scheduled.length).toBe(1) + expect(foundTasks.checklist.length).toBe(0) + expect(foundTasks.checklistDone.length).toBe(0) + expect(foundTasks.checklistCancelled.length).toBe(0) + expect(foundTasks.checklistScheduled.length).toBe(0) + }) + }) + + describe('getAllTasksFromCurrentNote', () => { + test('returns open tasks from current note', () => { + const currentNote = { + paragraphs: [ + { type: 'title', lineIndex: 0, content: 'Title', headingLevel: 1 }, + { type: 'empty', lineIndex: 1 }, + { type: 'open', lineIndex: 2, content: 'Open task 1' }, + { type: 'open', lineIndex: 3, content: 'Open task 2' }, + { type: 'done', lineIndex: 4, content: 'Done task' }, + ], + } + const tasks = getAllTasksFromCurrentNote(currentNote) + expect(tasks.length).toBe(3) + expect(tasks[0].content).toBe('Open task 1') + expect(tasks[1].content).toBe('Open task 2') + expect(tasks[2].content).toBe('Done task') + }) }) }) \ No newline at end of file diff --git a/mlevison.GenAITaskChecker/src/support/helpers.js b/mlevison.GenAITaskChecker/src/support/tasks.js similarity index 50% rename from mlevison.GenAITaskChecker/src/support/helpers.js rename to mlevison.GenAITaskChecker/src/support/tasks.js index ca321f04c..4a501f1cf 100644 --- a/mlevison.GenAITaskChecker/src/support/helpers.js +++ b/mlevison.GenAITaskChecker/src/support/tasks.js @@ -1,11 +1,18 @@ // @flow -// Here's an example function that can be imported and used in the plugin code -// More importantly, this function is pure (no NotePlan API calls), which means it can be tested + +import { getTasksByType } from '@helpers/sorting' + + // This is a good way to do much of your plugin work in isolation, with tests, and then the NPxxx files can be smaller // And just focus on NotePlan input/output, with the majority of the work happening here // Reminder: // About Flow: https://flow.org/en/docs/usage/#toc-write-flow-code // Getting started with Flow in NotePlan: https://github.com/NotePlan/plugins/blob/main/Flow_Guide.md -export function uppercase(str: string = ''): string { - return str.toUpperCase() -} + +export function getAllTasksFromCurrentNote(currentNote) { + // This function will extract all tasks from the current note and return them grouped by type + // It uses the getTasksByType function to categorize the tasks + const groupedTasks = getTasksByType(currentNote.paragraphs) + + return groupedTasks.open.concat(groupedTasks.done) +} \ No newline at end of file From 693623758b73d8fbb6077d90640b7ebf38ecba86 Mon Sep 17 00:00:00 2001 From: Mark Levison Date: Mon, 21 Jul 2025 15:57:15 -0400 Subject: [PATCH 04/10] Delete task code mlevison.plugin --- .../extractAllTasksFromCurrentNote.test.js | 100 ------------------ .../src/support/tasks.js | 18 ---- 2 files changed, 118 deletions(-) delete mode 100644 mlevison.GenAITaskChecker/__tests__/extractAllTasksFromCurrentNote.test.js delete mode 100644 mlevison.GenAITaskChecker/src/support/tasks.js diff --git a/mlevison.GenAITaskChecker/__tests__/extractAllTasksFromCurrentNote.test.js b/mlevison.GenAITaskChecker/__tests__/extractAllTasksFromCurrentNote.test.js deleted file mode 100644 index 12f0cd344..000000000 --- a/mlevison.GenAITaskChecker/__tests__/extractAllTasksFromCurrentNote.test.js +++ /dev/null @@ -1,100 +0,0 @@ -/* global jest, describe, test, expect, beforeAll, afterAll, beforeEach, afterEach */ -import { _ } from 'lodash' -import { getAllTasksFromCurrentNote } from '../src/support/tasks.js' -import * as sorting from '@helpers/sorting' -import { CustomConsole, LogType, LogMessage } from '@jest/console' // see note below -import { Calendar, Clipboard, CommandBar, DataStore, Editor, NotePlan, simpleFormatter, Paragraph /* Note, mockWasCalledWithString, Paragraph */ } from '@mocks/index' - - -beforeAll(() => { - global.Calendar = Calendar - global.Clipboard = Clipboard - global.CommandBar = CommandBar - global.DataStore = DataStore - global.Editor = Editor - global.NotePlan = NotePlan - global.console = new CustomConsole(process.stdout, process.stderr, simpleFormatter) // minimize log footprint - DataStore.settings['_logLevel'] = 'none' //change this to DEBUG to get more logging (or 'none' for none) -}) - -describe('mlevison.GenAITaskChecker', () => { - describe('getAllTasks using getTasksByType', () => { - test('prove Open and Cancelled Tasks found in sample note', () => { - const noteWIthOpenAndCancelledTasks = { - paragraphs: [ - { type: 'title', lineIndex: 0, content: 'Title', headingLevel: 1 }, - { type: 'empty', lineIndex: 1 }, - { type: 'title', lineIndex: 2, content: 'Tasks for 3.4.22', headingLevel: 2 }, - { type: 'open', lineIndex: 3, content: 'task 1' }, - { type: 'title', lineIndex: 4, content: 'Journal for 3.4.22' }, - { type: 'list', lineIndex: 5, content: 'first journal entry' }, - { type: 'list', lineIndex: 6, content: 'second journal entry' }, - { type: 'empty', lineIndex: 7 }, - { type: 'title', lineIndex: 8, content: 'Done ...', headingLevel: 2 }, - { type: 'title', lineIndex: 9, content: 'Cancelled', headingLevel: 2 }, - { type: 'cancelled', lineIndex: 10, content: 'task 4 not done' }, - ], - } - const foundTasks = sorting.getTasksByType(noteWIthOpenAndCancelledTasks.paragraphs) - - console.log('foundTasks open', foundTasks.open, foundTasks.done, foundTasks.cancelled) - - expect(foundTasks.open.length).toBe(1) - expect(foundTasks.done.length).toBe(0) - expect(foundTasks.cancelled.length).toBe(1) - expect(foundTasks.scheduled.length).toBe(0) - expect(foundTasks.checklist.length).toBe(0) - expect(foundTasks.checklistDone.length).toBe(0) - expect(foundTasks.checklistCancelled.length).toBe(0) - expect(foundTasks.checklistScheduled.length).toBe(0) - }) - - test('prove Done Tasks found in sample note', () => { - const noteWIthDoneAndCancelledTasks = { - paragraphs: [ - { type: 'title', lineIndex: 0, content: 'Title', headingLevel: 1 }, - { type: 'empty', lineIndex: 1 }, - { type: 'title', lineIndex: 2, content: 'Tasks for 3.4.22', headingLevel: 2 }, - { type: 'scheduled', lineIndex: 3, content: 'task 1' }, - { type: 'title', lineIndex: 4, content: 'Journal for 3.4.22' }, - { type: 'list', lineIndex: 5, content: 'first journal entry' }, - { type: 'list', lineIndex: 6, content: 'second journal entry' }, - { type: 'empty', lineIndex: 7 }, - { type: 'title', lineIndex: 8, content: 'Done ...', headingLevel: 2 }, - { type: 'done', lineIndex: 10, content: 'task finised' }, - ] - } - const foundTasks = sorting.getTasksByType(noteWIthDoneAndCancelledTasks.paragraphs) - - console.log('foundTasks open', foundTasks.open, foundTasks.done, foundTasks.cancelled) - - expect(foundTasks.open.length).toBe(0) - expect(foundTasks.done.length).toBe(1) - expect(foundTasks.cancelled.length).toBe(0) - expect(foundTasks.scheduled.length).toBe(1) - expect(foundTasks.checklist.length).toBe(0) - expect(foundTasks.checklistDone.length).toBe(0) - expect(foundTasks.checklistCancelled.length).toBe(0) - expect(foundTasks.checklistScheduled.length).toBe(0) - }) - }) - - describe('getAllTasksFromCurrentNote', () => { - test('returns open tasks from current note', () => { - const currentNote = { - paragraphs: [ - { type: 'title', lineIndex: 0, content: 'Title', headingLevel: 1 }, - { type: 'empty', lineIndex: 1 }, - { type: 'open', lineIndex: 2, content: 'Open task 1' }, - { type: 'open', lineIndex: 3, content: 'Open task 2' }, - { type: 'done', lineIndex: 4, content: 'Done task' }, - ], - } - const tasks = getAllTasksFromCurrentNote(currentNote) - expect(tasks.length).toBe(3) - expect(tasks[0].content).toBe('Open task 1') - expect(tasks[1].content).toBe('Open task 2') - expect(tasks[2].content).toBe('Done task') - }) - }) -}) \ No newline at end of file diff --git a/mlevison.GenAITaskChecker/src/support/tasks.js b/mlevison.GenAITaskChecker/src/support/tasks.js deleted file mode 100644 index 4a501f1cf..000000000 --- a/mlevison.GenAITaskChecker/src/support/tasks.js +++ /dev/null @@ -1,18 +0,0 @@ -// @flow - -import { getTasksByType } from '@helpers/sorting' - - -// This is a good way to do much of your plugin work in isolation, with tests, and then the NPxxx files can be smaller -// And just focus on NotePlan input/output, with the majority of the work happening here -// Reminder: -// About Flow: https://flow.org/en/docs/usage/#toc-write-flow-code -// Getting started with Flow in NotePlan: https://github.com/NotePlan/plugins/blob/main/Flow_Guide.md - -export function getAllTasksFromCurrentNote(currentNote) { - // This function will extract all tasks from the current note and return them grouped by type - // It uses the getTasksByType function to categorize the tasks - const groupedTasks = getTasksByType(currentNote.paragraphs) - - return groupedTasks.open.concat(groupedTasks.done) -} \ No newline at end of file From b627e5490a47408eb0e718f71498ea64df5723e8 Mon Sep 17 00:00:00 2001 From: Mark Levison Date: Mon, 21 Jul 2025 16:33:21 -0400 Subject: [PATCH 05/10] Added NoteFactory and Task Functions --- __mocks__/factories/noteFactory.js | 55 ++++++++++++++++++++ helpers/__tests__/tasks.test..js | 82 ++++++++++++++++++++++++++++++ helpers/tasks.js | 10 ++++ 3 files changed, 147 insertions(+) create mode 100644 __mocks__/factories/noteFactory.js create mode 100644 helpers/__tests__/tasks.test..js create mode 100644 helpers/tasks.js diff --git a/__mocks__/factories/noteFactory.js b/__mocks__/factories/noteFactory.js new file mode 100644 index 000000000..4ebac9431 --- /dev/null +++ b/__mocks__/factories/noteFactory.js @@ -0,0 +1,55 @@ +export const noteWIthOpenAndCancelledTasks = { + paragraphs: [ + { type: 'title', lineIndex: 0, content: 'Title', headingLevel: 1 }, + { type: 'empty', lineIndex: 1 }, + { type: 'title', lineIndex: 2, content: 'Tasks for 3.4.22', headingLevel: 2 }, + { type: 'open', lineIndex: 3, content: 'task 1' }, + { type: 'title', lineIndex: 4, content: 'Journal for 3.4.22' }, + { type: 'list', lineIndex: 5, content: 'first journal entry' }, + { type: 'list', lineIndex: 6, content: 'second journal entry' }, + { type: 'empty', lineIndex: 7 }, + { type: 'title', lineIndex: 8, content: 'Done ...', headingLevel: 2 }, + { type: 'title', lineIndex: 9, content: 'Cancelled', headingLevel: 2 }, + { type: 'cancelled', lineIndex: 10, content: 'task cancelled' }, + ], +} + +export const noteWIthDoneAndScheduledTasks = { + paragraphs: [ + { type: 'title', lineIndex: 0, content: 'Title', headingLevel: 1 }, + { type: 'empty', lineIndex: 1 }, + { type: 'title', lineIndex: 2, content: 'Tasks for 3.4.22', headingLevel: 2 }, + { type: 'scheduled', lineIndex: 3, content: 'task 1' }, + { type: 'title', lineIndex: 4, content: 'Journal for 3.4.22' }, + { type: 'list', lineIndex: 5, content: 'first journal entry' }, + { type: 'list', lineIndex: 6, content: 'second journal entry' }, + { type: 'empty', lineIndex: 7 }, + { type: 'title', lineIndex: 8, content: 'Done ...', headingLevel: 2 }, + { type: 'done', lineIndex: 10, content: 'task finished' }, + ], + } + + export const noteWithOpenAndDoneTasks = { + paragraphs: [ + { type: 'title', lineIndex: 0, content: 'Title', headingLevel: 1 }, + { type: 'empty', lineIndex: 1 }, + { type: 'open', lineIndex: 2, content: 'Open task 1' }, + { type: 'open', lineIndex: 3, content: 'Open task 2' }, + { type: 'done', lineIndex: 4, content: 'Done task' }, + ], + } + +export const noteWithOneTaskOfEachType = { + paragraphs: [ + { type: 'title', lineIndex: 0, content: 'Title', headingLevel: 1 }, + { type: 'empty', lineIndex: 1 }, + { type: 'open', lineIndex: 2, content: 'Open task 1' }, + { type: 'scheduled', lineIndex: 3, content: 'Scheduled task 2' }, + { type: 'done', lineIndex: 4, content: 'Done task' }, + { type: 'cancelled', lineIndex: 5, content: 'Cancelled task' }, + { type: 'checklist', lineIndex: 6, content: 'checklist' }, + { type: 'checklistDone', lineIndex: 7, content: 'checklistDone' }, + { type: 'checklistCancelled', lineIndex: 8, content: 'checklistCancelled' }, + { type: 'checklistScheduled', lineIndex: 9, content: 'checklistScheduled' }, + ], +} \ No newline at end of file diff --git a/helpers/__tests__/tasks.test..js b/helpers/__tests__/tasks.test..js new file mode 100644 index 000000000..e657d9670 --- /dev/null +++ b/helpers/__tests__/tasks.test..js @@ -0,0 +1,82 @@ +/* global jest, describe, test, expect, beforeAll, afterAll, beforeEach, afterEach */ +import { noteWIthOpenAndCancelledTasks, noteWIthDoneAndScheduledTasks, noteWithOpenAndDoneTasks, noteWithOneTaskOfEachType } from '@mocks/factories/noteFactory' + +import { getAllTasksFromCurrentNote } from '../tasks.js' +import * as sorting from '@helpers/sorting' +import { CustomConsole } from '@jest/console' // see note below +import { Calendar, Clipboard, CommandBar, DataStore, Editor, NotePlan, simpleFormatter, Paragraph /* Note, mockWasCalledWithString, Paragraph */ } from '@mocks/index' + +beforeAll(() => { + global.Calendar = Calendar + global.Clipboard = Clipboard + global.CommandBar = CommandBar + global.DataStore = DataStore + global.Editor = Editor + global.NotePlan = NotePlan + global.console = new CustomConsole(process.stdout, process.stderr, simpleFormatter) // minimize log footprint + DataStore.settings['_logLevel'] = 'none' //change this to DEBUG to get more logging (or 'none' for none) +}) + +describe('mlevison.GenAITaskChecker', () => { + describe('test getTasksByType', () => { + test('prove Open and Cancelled Tasks found in sample note', () => { + const foundTasks = sorting.getTasksByType(noteWIthOpenAndCancelledTasks.paragraphs) + + console.log('foundTasks open', foundTasks.open, foundTasks.done, foundTasks.cancelled) + + expect(foundTasks.open.length).toBe(1) + expect(foundTasks.done.length).toBe(0) + expect(foundTasks.cancelled.length).toBe(1) + expect(foundTasks.scheduled.length).toBe(0) + expect(foundTasks.checklist.length).toBe(0) + expect(foundTasks.checklistDone.length).toBe(0) + expect(foundTasks.checklistCancelled.length).toBe(0) + expect(foundTasks.checklistScheduled.length).toBe(0) + }) + + test('prove Done Tasks found in sample note', () => { + const foundTasks = sorting.getTasksByType(noteWIthDoneAndScheduledTasks.paragraphs) + + console.log('foundTasks open', foundTasks.open, foundTasks.done, foundTasks.cancelled) + + expect(foundTasks.open.length).toBe(0) + expect(foundTasks.done.length).toBe(1) + expect(foundTasks.cancelled.length).toBe(0) + expect(foundTasks.scheduled.length).toBe(1) + expect(foundTasks.checklist.length).toBe(0) + expect(foundTasks.checklistDone.length).toBe(0) + expect(foundTasks.checklistCancelled.length).toBe(0) + expect(foundTasks.checklistScheduled.length).toBe(0) + }) + }) + + describe('getAllTasksFromCurrentNote', () => { + test('returns open and done tasks from current note', () => { + const tasks = getAllTasksFromCurrentNote(noteWithOpenAndDoneTasks) + expect(tasks.length).toBe(3) + expect(tasks[0].content).toBe('Open task 1') + expect(tasks[1].content).toBe('Open task 2') + expect(tasks[2].content).toBe('Done task') + }) + + test('returns open and cancelled tasks from current note', () => { + const tasks = getAllTasksFromCurrentNote(noteWIthOpenAndCancelledTasks) + expect(tasks.length).toBe(2) + expect(tasks[0].content).toBe('task 1') + expect(tasks[1].content).toBe('task cancelled') + }) + + test('returns one of each task types from current note', () => { + const tasks = getAllTasksFromCurrentNote(noteWithOneTaskOfEachType) + expect(tasks.length).toBe(8) + expect(tasks[0].content).toBe('Open task 1') + expect(tasks[1].content).toBe('Scheduled task 2') + expect(tasks[2].content).toBe('Done task') + expect(tasks[3].content).toBe('Cancelled task') + expect(tasks[4].content).toBe('checklist') + expect(tasks[5].content).toBe('checklistScheduled') + expect(tasks[6].content).toBe('checklistDone') + expect(tasks[7].content).toBe('checklistCancelled') + }) + }) +}) diff --git a/helpers/tasks.js b/helpers/tasks.js new file mode 100644 index 000000000..651661742 --- /dev/null +++ b/helpers/tasks.js @@ -0,0 +1,10 @@ +import { getTasksByType } from './sorting' + +export function getAllTasksFromCurrentNote(currentNote) { + // This function will extract all tasks from the current note and return them grouped by type + // It uses the getTasksByType function to categorize the tasks + const groupedTasks = getTasksByType(currentNote.paragraphs) + + return groupedTasks.open.concat(groupedTasks.scheduled, groupedTasks.done, groupedTasks.cancelled, groupedTasks.checklist, groupedTasks.checklistScheduled, + groupedTasks.checklistDone, groupedTasks.checklistCancelled) +} \ No newline at end of file From 3ade7ad5ac87f4e535e5270ad16e0ac2b2e4c0e7 Mon Sep 17 00:00:00 2001 From: Mark Levison Date: Mon, 21 Jul 2025 17:04:44 -0400 Subject: [PATCH 06/10] Revert "Delete task code mlevison.plugin" This reverts commit 693623758b73d8fbb6077d90640b7ebf38ecba86. --- .../extractAllTasksFromCurrentNote.test.js | 100 ++++++++++++++++++ .../src/support/tasks.js | 18 ++++ 2 files changed, 118 insertions(+) create mode 100644 mlevison.GenAITaskChecker/__tests__/extractAllTasksFromCurrentNote.test.js create mode 100644 mlevison.GenAITaskChecker/src/support/tasks.js diff --git a/mlevison.GenAITaskChecker/__tests__/extractAllTasksFromCurrentNote.test.js b/mlevison.GenAITaskChecker/__tests__/extractAllTasksFromCurrentNote.test.js new file mode 100644 index 000000000..12f0cd344 --- /dev/null +++ b/mlevison.GenAITaskChecker/__tests__/extractAllTasksFromCurrentNote.test.js @@ -0,0 +1,100 @@ +/* global jest, describe, test, expect, beforeAll, afterAll, beforeEach, afterEach */ +import { _ } from 'lodash' +import { getAllTasksFromCurrentNote } from '../src/support/tasks.js' +import * as sorting from '@helpers/sorting' +import { CustomConsole, LogType, LogMessage } from '@jest/console' // see note below +import { Calendar, Clipboard, CommandBar, DataStore, Editor, NotePlan, simpleFormatter, Paragraph /* Note, mockWasCalledWithString, Paragraph */ } from '@mocks/index' + + +beforeAll(() => { + global.Calendar = Calendar + global.Clipboard = Clipboard + global.CommandBar = CommandBar + global.DataStore = DataStore + global.Editor = Editor + global.NotePlan = NotePlan + global.console = new CustomConsole(process.stdout, process.stderr, simpleFormatter) // minimize log footprint + DataStore.settings['_logLevel'] = 'none' //change this to DEBUG to get more logging (or 'none' for none) +}) + +describe('mlevison.GenAITaskChecker', () => { + describe('getAllTasks using getTasksByType', () => { + test('prove Open and Cancelled Tasks found in sample note', () => { + const noteWIthOpenAndCancelledTasks = { + paragraphs: [ + { type: 'title', lineIndex: 0, content: 'Title', headingLevel: 1 }, + { type: 'empty', lineIndex: 1 }, + { type: 'title', lineIndex: 2, content: 'Tasks for 3.4.22', headingLevel: 2 }, + { type: 'open', lineIndex: 3, content: 'task 1' }, + { type: 'title', lineIndex: 4, content: 'Journal for 3.4.22' }, + { type: 'list', lineIndex: 5, content: 'first journal entry' }, + { type: 'list', lineIndex: 6, content: 'second journal entry' }, + { type: 'empty', lineIndex: 7 }, + { type: 'title', lineIndex: 8, content: 'Done ...', headingLevel: 2 }, + { type: 'title', lineIndex: 9, content: 'Cancelled', headingLevel: 2 }, + { type: 'cancelled', lineIndex: 10, content: 'task 4 not done' }, + ], + } + const foundTasks = sorting.getTasksByType(noteWIthOpenAndCancelledTasks.paragraphs) + + console.log('foundTasks open', foundTasks.open, foundTasks.done, foundTasks.cancelled) + + expect(foundTasks.open.length).toBe(1) + expect(foundTasks.done.length).toBe(0) + expect(foundTasks.cancelled.length).toBe(1) + expect(foundTasks.scheduled.length).toBe(0) + expect(foundTasks.checklist.length).toBe(0) + expect(foundTasks.checklistDone.length).toBe(0) + expect(foundTasks.checklistCancelled.length).toBe(0) + expect(foundTasks.checklistScheduled.length).toBe(0) + }) + + test('prove Done Tasks found in sample note', () => { + const noteWIthDoneAndCancelledTasks = { + paragraphs: [ + { type: 'title', lineIndex: 0, content: 'Title', headingLevel: 1 }, + { type: 'empty', lineIndex: 1 }, + { type: 'title', lineIndex: 2, content: 'Tasks for 3.4.22', headingLevel: 2 }, + { type: 'scheduled', lineIndex: 3, content: 'task 1' }, + { type: 'title', lineIndex: 4, content: 'Journal for 3.4.22' }, + { type: 'list', lineIndex: 5, content: 'first journal entry' }, + { type: 'list', lineIndex: 6, content: 'second journal entry' }, + { type: 'empty', lineIndex: 7 }, + { type: 'title', lineIndex: 8, content: 'Done ...', headingLevel: 2 }, + { type: 'done', lineIndex: 10, content: 'task finised' }, + ] + } + const foundTasks = sorting.getTasksByType(noteWIthDoneAndCancelledTasks.paragraphs) + + console.log('foundTasks open', foundTasks.open, foundTasks.done, foundTasks.cancelled) + + expect(foundTasks.open.length).toBe(0) + expect(foundTasks.done.length).toBe(1) + expect(foundTasks.cancelled.length).toBe(0) + expect(foundTasks.scheduled.length).toBe(1) + expect(foundTasks.checklist.length).toBe(0) + expect(foundTasks.checklistDone.length).toBe(0) + expect(foundTasks.checklistCancelled.length).toBe(0) + expect(foundTasks.checklistScheduled.length).toBe(0) + }) + }) + + describe('getAllTasksFromCurrentNote', () => { + test('returns open tasks from current note', () => { + const currentNote = { + paragraphs: [ + { type: 'title', lineIndex: 0, content: 'Title', headingLevel: 1 }, + { type: 'empty', lineIndex: 1 }, + { type: 'open', lineIndex: 2, content: 'Open task 1' }, + { type: 'open', lineIndex: 3, content: 'Open task 2' }, + { type: 'done', lineIndex: 4, content: 'Done task' }, + ], + } + const tasks = getAllTasksFromCurrentNote(currentNote) + expect(tasks.length).toBe(3) + expect(tasks[0].content).toBe('Open task 1') + expect(tasks[1].content).toBe('Open task 2') + expect(tasks[2].content).toBe('Done task') + }) + }) +}) \ No newline at end of file diff --git a/mlevison.GenAITaskChecker/src/support/tasks.js b/mlevison.GenAITaskChecker/src/support/tasks.js new file mode 100644 index 000000000..4a501f1cf --- /dev/null +++ b/mlevison.GenAITaskChecker/src/support/tasks.js @@ -0,0 +1,18 @@ +// @flow + +import { getTasksByType } from '@helpers/sorting' + + +// This is a good way to do much of your plugin work in isolation, with tests, and then the NPxxx files can be smaller +// And just focus on NotePlan input/output, with the majority of the work happening here +// Reminder: +// About Flow: https://flow.org/en/docs/usage/#toc-write-flow-code +// Getting started with Flow in NotePlan: https://github.com/NotePlan/plugins/blob/main/Flow_Guide.md + +export function getAllTasksFromCurrentNote(currentNote) { + // This function will extract all tasks from the current note and return them grouped by type + // It uses the getTasksByType function to categorize the tasks + const groupedTasks = getTasksByType(currentNote.paragraphs) + + return groupedTasks.open.concat(groupedTasks.done) +} \ No newline at end of file From d58b4e9a14bd7f497293fbfd79e6ccb2718ed6a4 Mon Sep 17 00:00:00 2001 From: Mark Levison Date: Mon, 21 Jul 2025 17:04:58 -0400 Subject: [PATCH 07/10] Revert "Add limited version of getAllTasksForCurrentNote" This reverts commit 9cf6d954e36c9302ebfe2ebd8b19ee0bb5db3302. --- .../extractAllTasksFromCurrentNote.test.js | 68 ++----------------- .../src/support/{tasks.js => helpers.js} | 17 ++--- 2 files changed, 12 insertions(+), 73 deletions(-) rename mlevison.GenAITaskChecker/src/support/{tasks.js => helpers.js} (50%) diff --git a/mlevison.GenAITaskChecker/__tests__/extractAllTasksFromCurrentNote.test.js b/mlevison.GenAITaskChecker/__tests__/extractAllTasksFromCurrentNote.test.js index 12f0cd344..eabd9c793 100644 --- a/mlevison.GenAITaskChecker/__tests__/extractAllTasksFromCurrentNote.test.js +++ b/mlevison.GenAITaskChecker/__tests__/extractAllTasksFromCurrentNote.test.js @@ -1,7 +1,6 @@ /* global jest, describe, test, expect, beforeAll, afterAll, beforeEach, afterEach */ import { _ } from 'lodash' -import { getAllTasksFromCurrentNote } from '../src/support/tasks.js' -import * as sorting from '@helpers/sorting' +import * as sorting from '../../helpers/sorting' import { CustomConsole, LogType, LogMessage } from '@jest/console' // see note below import { Calendar, Clipboard, CommandBar, DataStore, Editor, NotePlan, simpleFormatter, Paragraph /* Note, mockWasCalledWithString, Paragraph */ } from '@mocks/index' @@ -14,15 +13,15 @@ beforeAll(() => { global.Editor = Editor global.NotePlan = NotePlan global.console = new CustomConsole(process.stdout, process.stderr, simpleFormatter) // minimize log footprint - DataStore.settings['_logLevel'] = 'none' //change this to DEBUG to get more logging (or 'none' for none) + DataStore.settings['_logLevel'] = 'DEBUG' //change this to DEBUG to get more logging (or 'none' for none) }) describe('mlevison.GenAITaskChecker', () => { - describe('getAllTasks using getTasksByType', () => { - test('prove Open and Cancelled Tasks found in sample note', () => { - const noteWIthOpenAndCancelledTasks = { + describe('getAllTasks', () => { + test('prove All Tasks found in sample note', () => { + const noteA = { paragraphs: [ - { type: 'title', lineIndex: 0, content: 'Title', headingLevel: 1 }, + { type: 'title', lineIndex: 0, content: 'NoteA Title', headingLevel: 1 }, { type: 'empty', lineIndex: 1 }, { type: 'title', lineIndex: 2, content: 'Tasks for 3.4.22', headingLevel: 2 }, { type: 'open', lineIndex: 3, content: 'task 1' }, @@ -35,66 +34,13 @@ describe('mlevison.GenAITaskChecker', () => { { type: 'cancelled', lineIndex: 10, content: 'task 4 not done' }, ], } - const foundTasks = sorting.getTasksByType(noteWIthOpenAndCancelledTasks.paragraphs) + const foundTasks = sorting.getTasksByType(noteA.paragraphs) console.log('foundTasks open', foundTasks.open, foundTasks.done, foundTasks.cancelled) expect(foundTasks.open.length).toBe(1) expect(foundTasks.done.length).toBe(0) expect(foundTasks.cancelled.length).toBe(1) - expect(foundTasks.scheduled.length).toBe(0) - expect(foundTasks.checklist.length).toBe(0) - expect(foundTasks.checklistDone.length).toBe(0) - expect(foundTasks.checklistCancelled.length).toBe(0) - expect(foundTasks.checklistScheduled.length).toBe(0) }) - - test('prove Done Tasks found in sample note', () => { - const noteWIthDoneAndCancelledTasks = { - paragraphs: [ - { type: 'title', lineIndex: 0, content: 'Title', headingLevel: 1 }, - { type: 'empty', lineIndex: 1 }, - { type: 'title', lineIndex: 2, content: 'Tasks for 3.4.22', headingLevel: 2 }, - { type: 'scheduled', lineIndex: 3, content: 'task 1' }, - { type: 'title', lineIndex: 4, content: 'Journal for 3.4.22' }, - { type: 'list', lineIndex: 5, content: 'first journal entry' }, - { type: 'list', lineIndex: 6, content: 'second journal entry' }, - { type: 'empty', lineIndex: 7 }, - { type: 'title', lineIndex: 8, content: 'Done ...', headingLevel: 2 }, - { type: 'done', lineIndex: 10, content: 'task finised' }, - ] - } - const foundTasks = sorting.getTasksByType(noteWIthDoneAndCancelledTasks.paragraphs) - - console.log('foundTasks open', foundTasks.open, foundTasks.done, foundTasks.cancelled) - - expect(foundTasks.open.length).toBe(0) - expect(foundTasks.done.length).toBe(1) - expect(foundTasks.cancelled.length).toBe(0) - expect(foundTasks.scheduled.length).toBe(1) - expect(foundTasks.checklist.length).toBe(0) - expect(foundTasks.checklistDone.length).toBe(0) - expect(foundTasks.checklistCancelled.length).toBe(0) - expect(foundTasks.checklistScheduled.length).toBe(0) - }) - }) - - describe('getAllTasksFromCurrentNote', () => { - test('returns open tasks from current note', () => { - const currentNote = { - paragraphs: [ - { type: 'title', lineIndex: 0, content: 'Title', headingLevel: 1 }, - { type: 'empty', lineIndex: 1 }, - { type: 'open', lineIndex: 2, content: 'Open task 1' }, - { type: 'open', lineIndex: 3, content: 'Open task 2' }, - { type: 'done', lineIndex: 4, content: 'Done task' }, - ], - } - const tasks = getAllTasksFromCurrentNote(currentNote) - expect(tasks.length).toBe(3) - expect(tasks[0].content).toBe('Open task 1') - expect(tasks[1].content).toBe('Open task 2') - expect(tasks[2].content).toBe('Done task') - }) }) }) \ No newline at end of file diff --git a/mlevison.GenAITaskChecker/src/support/tasks.js b/mlevison.GenAITaskChecker/src/support/helpers.js similarity index 50% rename from mlevison.GenAITaskChecker/src/support/tasks.js rename to mlevison.GenAITaskChecker/src/support/helpers.js index 4a501f1cf..ca321f04c 100644 --- a/mlevison.GenAITaskChecker/src/support/tasks.js +++ b/mlevison.GenAITaskChecker/src/support/helpers.js @@ -1,18 +1,11 @@ // @flow - -import { getTasksByType } from '@helpers/sorting' - - +// Here's an example function that can be imported and used in the plugin code +// More importantly, this function is pure (no NotePlan API calls), which means it can be tested // This is a good way to do much of your plugin work in isolation, with tests, and then the NPxxx files can be smaller // And just focus on NotePlan input/output, with the majority of the work happening here // Reminder: // About Flow: https://flow.org/en/docs/usage/#toc-write-flow-code // Getting started with Flow in NotePlan: https://github.com/NotePlan/plugins/blob/main/Flow_Guide.md - -export function getAllTasksFromCurrentNote(currentNote) { - // This function will extract all tasks from the current note and return them grouped by type - // It uses the getTasksByType function to categorize the tasks - const groupedTasks = getTasksByType(currentNote.paragraphs) - - return groupedTasks.open.concat(groupedTasks.done) -} \ No newline at end of file +export function uppercase(str: string = ''): string { + return str.toUpperCase() +} From 2e92085d7860d81cc6f6b5210e0a20fb30d3b2ee Mon Sep 17 00:00:00 2001 From: Mark Levison Date: Mon, 21 Jul 2025 17:05:06 -0400 Subject: [PATCH 08/10] Revert "Plugin Initialized and First Test Case working" This reverts commit a729394681cdfe8c3b40543099dea51c552215db. --- ...ACTIVE.notjs => NPPluginMain.NOTACTIVE.js} | 2 +- .../extractAllTasksFromCurrentNote.test.js | 46 ------------ .../sample files/TestNoteWithTasks.md | 71 ------------------- 3 files changed, 1 insertion(+), 118 deletions(-) rename mlevison.GenAITaskChecker/__tests__/{NPPluginMain.NOTACTIVE.notjs => NPPluginMain.NOTACTIVE.js} (99%) delete mode 100644 mlevison.GenAITaskChecker/__tests__/extractAllTasksFromCurrentNote.test.js delete mode 100644 mlevison.GenAITaskChecker/__tests__/sample files/TestNoteWithTasks.md diff --git a/mlevison.GenAITaskChecker/__tests__/NPPluginMain.NOTACTIVE.notjs b/mlevison.GenAITaskChecker/__tests__/NPPluginMain.NOTACTIVE.js similarity index 99% rename from mlevison.GenAITaskChecker/__tests__/NPPluginMain.NOTACTIVE.notjs rename to mlevison.GenAITaskChecker/__tests__/NPPluginMain.NOTACTIVE.js index 657fe3098..f0d7b79e3 100644 --- a/mlevison.GenAITaskChecker/__tests__/NPPluginMain.NOTACTIVE.notjs +++ b/mlevison.GenAITaskChecker/__tests__/NPPluginMain.NOTACTIVE.js @@ -7,7 +7,7 @@ // Jest testing docs: https://jestjs.io/docs/using-matchers /* eslint-disable */ - +import * as f from '../src/sortTasks' import { CustomConsole, LogType, LogMessage } from '@jest/console' // see note below import { Calendar, Clipboard, CommandBar, DataStore, Editor, NotePlan, simpleFormatter /* Note, mockWasCalledWithString, Paragraph */ } from '@mocks/index' diff --git a/mlevison.GenAITaskChecker/__tests__/extractAllTasksFromCurrentNote.test.js b/mlevison.GenAITaskChecker/__tests__/extractAllTasksFromCurrentNote.test.js deleted file mode 100644 index eabd9c793..000000000 --- a/mlevison.GenAITaskChecker/__tests__/extractAllTasksFromCurrentNote.test.js +++ /dev/null @@ -1,46 +0,0 @@ -/* global jest, describe, test, expect, beforeAll, afterAll, beforeEach, afterEach */ -import { _ } from 'lodash' -import * as sorting from '../../helpers/sorting' -import { CustomConsole, LogType, LogMessage } from '@jest/console' // see note below -import { Calendar, Clipboard, CommandBar, DataStore, Editor, NotePlan, simpleFormatter, Paragraph /* Note, mockWasCalledWithString, Paragraph */ } from '@mocks/index' - - -beforeAll(() => { - global.Calendar = Calendar - global.Clipboard = Clipboard - global.CommandBar = CommandBar - global.DataStore = DataStore - global.Editor = Editor - global.NotePlan = NotePlan - global.console = new CustomConsole(process.stdout, process.stderr, simpleFormatter) // minimize log footprint - DataStore.settings['_logLevel'] = 'DEBUG' //change this to DEBUG to get more logging (or 'none' for none) -}) - -describe('mlevison.GenAITaskChecker', () => { - describe('getAllTasks', () => { - test('prove All Tasks found in sample note', () => { - const noteA = { - paragraphs: [ - { type: 'title', lineIndex: 0, content: 'NoteA Title', headingLevel: 1 }, - { type: 'empty', lineIndex: 1 }, - { type: 'title', lineIndex: 2, content: 'Tasks for 3.4.22', headingLevel: 2 }, - { type: 'open', lineIndex: 3, content: 'task 1' }, - { type: 'title', lineIndex: 4, content: 'Journal for 3.4.22' }, - { type: 'list', lineIndex: 5, content: 'first journal entry' }, - { type: 'list', lineIndex: 6, content: 'second journal entry' }, - { type: 'empty', lineIndex: 7 }, - { type: 'title', lineIndex: 8, content: 'Done ...', headingLevel: 2 }, - { type: 'title', lineIndex: 9, content: 'Cancelled', headingLevel: 2 }, - { type: 'cancelled', lineIndex: 10, content: 'task 4 not done' }, - ], - } - const foundTasks = sorting.getTasksByType(noteA.paragraphs) - - console.log('foundTasks open', foundTasks.open, foundTasks.done, foundTasks.cancelled) - - expect(foundTasks.open.length).toBe(1) - expect(foundTasks.done.length).toBe(0) - expect(foundTasks.cancelled.length).toBe(1) - }) - }) -}) \ No newline at end of file diff --git a/mlevison.GenAITaskChecker/__tests__/sample files/TestNoteWithTasks.md b/mlevison.GenAITaskChecker/__tests__/sample files/TestNoteWithTasks.md deleted file mode 100644 index 2f592c061..000000000 --- a/mlevison.GenAITaskChecker/__tests__/sample files/TestNoteWithTasks.md +++ /dev/null @@ -1,71 +0,0 @@ -# Theme Test Note (for @jgclark themes) -# H1 colour and size -## H2 colour and size -### H3 colour and size -#### H4 colour and size -### Other things that are part of standard themes -With both **bold** and _italic_ text, and a ***combination***. -From v3.9.4 we can now have ==some nicer highlighting== or ~~strikethrough~~ around things. Even ~underlining~ _if we must_. -- #test #tags and @mentions are highlighted (very slightly differently) and have their links turned off -- here's `some inline code` and body text -* ! do -* !! something -* !!! very important -* >> working on something -* [>] ! Important Todo in the future >2021-08-04 -* [>] Scheduled into the future >2031-03-02 -* [x] Completed todo with #tagging and @ztag/subtag(test) @done(2021-02-13) -* [-] Cancelled todo with [How to be Generous to a Friend With Depression](https://www.stewardship.org.uk/blog/blog/post/733-how-to-be-generous-to-a-friend-with-depression?utm_source=Stewardship) -+ Checklist open -+ [>] Checklist scheduled and with a sync marker ^7qfqph -+ [x] Checklist done -+ [-] Checklist cancelled -> Every disease that submits to a cure shall be cured: but we will not call blue yellow to please those who insist on still having jaundice, nor make a midden of the world’s garden for the sake of some who cannot abide the smell of roses.’ -- C. S. Lewis (The Great Divorce) -- Bullets go like this - - and then this -Frontmatter like fields: look like this. ---- -### Markdown Links -The markdown syntax in Markdown links gets hidden, but appear when cursor is in the link: [NotePlan website](https://noteplan.co/). -### Note Links -- Standard Note links are shown without the surrounding brackets: [[Blank Header TEST]] -- Arrow note links are shown as above, but with a slightly different background colour. e.g. >Blank Header TEST<. They avoid the back-reference showing in that page's References section. (See also [How to create a link without a backlink?](https://help.noteplan.co/article/143-how-to-create-a-link-without-a-backlink#arrowlinks)) -### Time Blocks -For people using the AutoTimeBlocking plugin with the default settings, this hides the tag `#🕑` which `/atb` includes on lines it creates: #🕑 -### Comments -You can hide comments until you move your cursor into them, either inline like this: %%something in the middle%%, or at the end of a line like this: // something at the end -### Special highlight -Some people have a special use for a highlight that runs until the end of the line. To do this insert a Pilcrow (Alt-7) ¶ and see the effect. -### Fields and attributes -Fields: also supported, which might be useful for front matter, until fuller support for theming that emerges. -- Note: fields only apply at the start of a text line (not header, bullet etc.) -Attributes:: also can be highlighted at the start of a line. -### Tags in Template definitions -Here's a active tag: <% template tag %> and one that is commented out: <%# commented out template tag %> . -### Code Block -```markdown -## Heading -Test code block :+\/ -01234567890 ABCEDFGHijklmnopqrstu *with* **emphasis** and `things`. -1. list -* bullet -``` ---- -### More info -- [Customize Themes - NotePlan Knowledge Base](https://help.noteplan.co/article/44-customize-themes) -- [Theme Changelog - NotePlan Knowledge Base](https://help.noteplan.co/article/211-theme-changelog) -- My repo is [GitHub - jgclark/NP-themes: My NotePlan themes](https://github.com/jgclark/NP-themes) -- Brokosz has a repo at [GitHub - brokosz/NotePlan_Themes: Small collection of custom themes for NotePlan 3](https://github.com/brokosz/NotePlan_Themes) - ---- -## Full para breaks -Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore. - -Et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. - -Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. -## Single newline breaks -Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore. -Et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. -Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. -![📅](2023-10-26 14:00:::8A2C2207-98C9-40C1-9489-242CFFF95435:::NA:::1CB Team #meeting:::#1BADF8) From 60a351806660b6b471724de9be9a6bb341ffc45d Mon Sep 17 00:00:00 2001 From: Mark Levison Date: Mon, 21 Jul 2025 17:05:14 -0400 Subject: [PATCH 09/10] Revert "Initial Checkin GenAITaskChecker" This reverts commit 42130d675bc8e7a8f4ee82cb31bb9d2aa647cfff. --- mlevison.GenAITaskChecker/CHANGELOG.md | 27 ---- mlevison.GenAITaskChecker/README.md | 41 ------ .../__tests__/NPPluginMain.NOTACTIVE.js | 124 ------------------ mlevison.GenAITaskChecker/plugin.json | 113 ---------------- .../requiredFiles/html-plugin-comms.js | 106 --------------- .../src/NPMessagesFromHTMLWindow.js | 67 ---------- mlevison.GenAITaskChecker/src/NPPluginMain.js | 75 ----------- .../src/NPTriggers-Hooks.js | 119 ----------------- mlevison.GenAITaskChecker/src/index.js | 35 ----- .../src/support/fetchOverrides.js | 57 -------- .../google.search-for-something.json | 4 - .../src/support/helpers.js | 11 -- 12 files changed, 779 deletions(-) delete mode 100644 mlevison.GenAITaskChecker/CHANGELOG.md delete mode 100644 mlevison.GenAITaskChecker/README.md delete mode 100644 mlevison.GenAITaskChecker/__tests__/NPPluginMain.NOTACTIVE.js delete mode 100644 mlevison.GenAITaskChecker/plugin.json delete mode 100644 mlevison.GenAITaskChecker/requiredFiles/html-plugin-comms.js delete mode 100644 mlevison.GenAITaskChecker/src/NPMessagesFromHTMLWindow.js delete mode 100644 mlevison.GenAITaskChecker/src/NPPluginMain.js delete mode 100644 mlevison.GenAITaskChecker/src/NPTriggers-Hooks.js delete mode 100644 mlevison.GenAITaskChecker/src/index.js delete mode 100644 mlevison.GenAITaskChecker/src/support/fetchOverrides.js delete mode 100644 mlevison.GenAITaskChecker/src/support/fetchResponses/google.search-for-something.json delete mode 100644 mlevison.GenAITaskChecker/src/support/helpers.js diff --git a/mlevison.GenAITaskChecker/CHANGELOG.md b/mlevison.GenAITaskChecker/CHANGELOG.md deleted file mode 100644 index b200584b7..000000000 --- a/mlevison.GenAITaskChecker/CHANGELOG.md +++ /dev/null @@ -1,27 +0,0 @@ -# mlevison.GenAITaskChecker Changelog - -## About mlevison.GenAITaskChecker Plugin - -See Plugin [README](https://github.com/NotePlan/plugins/blob/main/mlevison.GenAITaskChecker/README.md) for details on available commands and use case. - -## [x.x.x] - yyyy-mm-dd (githubUserName) - -### Added -List what has been added. If nothing has been changed, this section can be removed. - -### Changed -List what has changed. If nothing has been changed, this section can be removed. - -### Removed -List what has removed. If nothing has been removed, this section can be removed. - -## Changelog - -All notable changes to this project will be documented in this file. - -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - -## Plugin Versioning Uses Semver - -All NotePlan plugins follow `semver` versioning. For details, please refer to [semver website](https://semver.org/) diff --git a/mlevison.GenAITaskChecker/README.md b/mlevison.GenAITaskChecker/README.md deleted file mode 100644 index 13fe32a94..000000000 --- a/mlevison.GenAITaskChecker/README.md +++ /dev/null @@ -1,41 +0,0 @@ -# GenAITaskChecker Noteplan Plugin - -## Latest Updates - -See [CHANGELOG](https://github.com/NotePlan/plugins/blob/main/mlevison.GenAITaskChecker/CHANGELOG.md) for latest updates/changes to this plugin. - -## About This Plugin - -Finds all tasks in the current note and submits them the LLM asking if the tasks are aligned with quarterly plan. - -[You will delete this text and replace it with a readme about your plugin -- not ever seen by users, but good for people looking at your code. Before you delete though, you should know:] - -You do not need all of this scaffolding for a basic NP plugin. As the instructions state [Creating Plugins](https://help.noteplan.co/article/65-commandbar-plugins), you can create a plugin with just two files: `plugin.json` and `script.js`. Please read that whole page before proceeding here. - -However, for more complex plugins, you may find that it's easier to write code in multiple files, incorporating code (helper functions, etc.) written (and *TESTED*) previously by others. We strongly recommend type checking (e.g. [Flow.io](https://flow.io)) to help validate the code you write. If either of those is interesting to you, you're in the right place. Before going any further, make sure you follow the development environment [setup instructions](https://github.com/NotePlan/plugins). - -## Creating NotePlan Plugin - -You can create a NotePlan plugin by executing: - -```bash -noteplan-cli plugin:create -``` - -Open up a terminal folder and change directory to the plugins repository root. Run the command `npm run autowatch` which will keep looking for changes to all plugin files and will re-compile when JavaScript changes are made. It will also transpile ES6 and ES7 code to ES5 which will run on virtually all Macs, and will copy the file(s) to the NotePlan Plugins folder, so you can immediately test in Noteplan. - -### NotePlan Plugins Directory -You can find all your currently installed NotePlan Plugins here (for AppStore version of NotePlan): - -```bash -/Users//Library/Containers/co.noteplan.NotePlan3/Data/Library/Application Support/co.noteplan.NotePlan3/Plugins -``` - -Keep in mind that you can code/test without updating the plugin version property in `plugin.json`, however when you push the code to the Plugins repository (or create a PR), you should update the version number so that other NotePlan users who have installed your plugin will know that an updated version is available. - -Further to that point, you can use your plugin locally, or you can use `git` to create a Pull Request to get it merged in the Noteplan/plugins repository and potentially available for all users through the `NotePlan > Preferences > Plugins` tab. - -That's it. Happy coding! - -## NotePlan Plugin Team -Hat-tip to @eduard, @nmn, @jgclark, @dwertheimer and @codedungeon, who made all this fancy cool stuff. diff --git a/mlevison.GenAITaskChecker/__tests__/NPPluginMain.NOTACTIVE.js b/mlevison.GenAITaskChecker/__tests__/NPPluginMain.NOTACTIVE.js deleted file mode 100644 index f0d7b79e3..000000000 --- a/mlevison.GenAITaskChecker/__tests__/NPPluginMain.NOTACTIVE.js +++ /dev/null @@ -1,124 +0,0 @@ -/* - * THIS FILE SHOULD BE RENAMED WITH ".test.js" AT THE END SO JEST WILL FIND AND RUN IT - * It is included here as an example/starting point for your own tests - */ - -/* global jest, describe, test, expect, beforeAll, afterAll, beforeEach, afterEach */ -// Jest testing docs: https://jestjs.io/docs/using-matchers -/* eslint-disable */ - -import * as f from '../src/sortTasks' -import { CustomConsole, LogType, LogMessage } from '@jest/console' // see note below -import { Calendar, Clipboard, CommandBar, DataStore, Editor, NotePlan, simpleFormatter /* Note, mockWasCalledWithString, Paragraph */ } from '@mocks/index' - -const PLUGIN_NAME = `{{pluginID}}` -const FILENAME = `NPPluginMain` - -beforeAll(() => { - global.Calendar = Calendar - global.Clipboard = Clipboard - global.CommandBar = CommandBar - global.DataStore = DataStore - global.Editor = Editor - global.NotePlan = NotePlan - global.console = new CustomConsole(process.stdout, process.stderr, simpleFormatter) // minimize log footprint - DataStore.settings['_logLevel'] = 'none' //change this to DEBUG to get more logging (or 'none' for none) -}) - -/* Samples: -expect(result).toMatch(/someString/) -expect(result).not.toMatch(/someString/) -expect(result).toEqual([]) -import { mockWasCalledWith } from '@mocks/mockHelpers' - const spy = jest.spyOn(console, 'log') - const result = mainFile.getConfig() - expect(mockWasCalledWith(spy, /config was empty/)).toBe(true) - spy.mockRestore() - - test('should return the command object', () => { - const result = f.getPluginCommands({ 'plugin.commands': [{ a: 'foo' }] }) - expect(result).toEqual([{ a: 'foo' }]) - }) -*/ - -describe('mlevison.GenAITaskChecker' /* pluginID */, () => { - describe('NPPluginMain' /* file */, () => { - describe('sayHello' /* function */, () => { - test('should insert text if called with a string param', async () => { - // tests start with "should" to describe the expected behavior - const spy = jest.spyOn(Editor, 'insertTextAtCursor') - const result = await mainFile.sayHello('Testing...') - expect(spy).toHaveBeenCalled() - expect(spy).toHaveBeenNthCalledWith( - 1, - `***You clicked the link!*** The message at the end of the link is "Testing...". Now the rest of the plugin will run just as before...\n\n`, - ) - spy.mockRestore() - }) - test('should write result to console', async () => { - // tests start with "should" to describe the expected behavior - const spy = jest.spyOn(console, 'log') - const result = await mainFile.sayHello() - expect(spy).toHaveBeenCalled() - expect(spy).toHaveBeenNthCalledWith(1, expect.stringMatching(/The plugin says: HELLO WORLD FROM TEST PLUGIN!/)) - spy.mockRestore() - }) - test('should call DataStore.settings', async () => { - // tests start with "should" to describe the expected behavior - const oldValue = DataStore.settings - DataStore.settings = { settingsString: 'settingTest' } - const spy = jest.spyOn(Editor, 'insertTextAtCursor') - const _ = await mainFile.sayHello() - expect(spy).toHaveBeenCalled() - expect(spy).toHaveBeenNthCalledWith(1, expect.stringMatching(/settingTest/)) - DataStore.settings = oldValue - spy.mockRestore() - }) - test('should call DataStore.settings if no value set', async () => { - // tests start with "should" to describe the expected behavior - const oldValue = DataStore.settings - DataStore.settings = { settingsString: undefined } - const spy = jest.spyOn(Editor, 'insertTextAtCursor') - const _ = await mainFile.sayHello() - expect(spy).toHaveBeenCalled() - expect(spy).toHaveBeenNthCalledWith(1, expect.stringMatching(/\*\*\"\"\*\*/)) - DataStore.settings = oldValue - spy.mockRestore() - }) - test('should CLO write note.paragraphs to console', async () => { - // tests start with "should" to describe the expected behavior - const prevEditorNoteValue = copyObject(Editor.note || {}) - Editor.note = new Note({ filename: 'testingFile' }) - Editor.note.paragraphs = [new Paragraph({ content: 'testingParagraph' })] - const spy = jest.spyOn(console, 'log') - const result = await mainFile.sayHello() - expect(spy).toHaveBeenCalled() - expect(spy).toHaveBeenNthCalledWith(2, expect.stringMatching(/\"content\": \"testingParagraph\"/)) - Editor.note = prevEditorNoteValue - spy.mockRestore() - }) - test('should insert a link if not called with a string param', async () => { - // tests start with "should" to describe the expected behavior - const spy = jest.spyOn(Editor, 'insertTextAtCursor') - const result = await mainFile.sayHello('') - expect(spy).toHaveBeenCalled() - expect(spy).toHaveBeenLastCalledWith(expect.stringMatching(/noteplan:\/\/x-callback-url\/runPlugin/)) - spy.mockRestore() - }) - test('should write an error to console on throw', async () => { - // tests start with "should" to describe the expected behavior - const spy = jest.spyOn(console, 'log') - const oldValue = Editor.insertTextAtCursor - delete Editor.insertTextAtCursor - try { - const result = await mainFile.sayHello() - } catch (e) { - expect(e.message).stringMatching(/ERROR/) - } - expect(spy).toHaveBeenCalledWith(expect.stringMatching(/ERROR/)) - Editor.insertTextAtCursor = oldValue - spy.mockRestore() - }) - }) - }) -}) diff --git a/mlevison.GenAITaskChecker/plugin.json b/mlevison.GenAITaskChecker/plugin.json deleted file mode 100644 index 3933fd7af..000000000 --- a/mlevison.GenAITaskChecker/plugin.json +++ /dev/null @@ -1,113 +0,0 @@ -{ - "COMMENT": "Details on these fields: https://help.noteplan.co/article/67-create-command-bar-plugins", - "macOS.minVersion": "10.13.0", - "noteplan.minAppVersion": "3.4.0", - "plugin.id": "mlevison.GenAITaskChecker", - "plugin.name": "🧩 GenAITaskChecker", - "plugin.version": "0.1.0", - "plugin.lastUpdateInfo": "Describe this update", - "plugin.description": "Finds all tasks in the current note and submits them the LLM asking if the tasks are aligned with quarterly plan.", - "plugin.author": "Agile Pain Relief Consulting", - "plugin.requiredFiles-EDIT_ME": ["html-plugin-comms.js"], - "plugin.requiredFiles-NOTE": "If you want to use HTML windows, remove the '-EDIT_ME' ABOVE", - "plugin.dependsOn": [], - "plugin.script": "script.js", - "plugin.url": "https://github.com/NotePlan/plugins/blob/main/mlevison.GenAITaskChecker/README.md", - "plugin.changelog": "https://github.com/NotePlan/plugins/blob/main/mlevison.GenAITaskChecker/CHANGELOG.md", - "plugin.commands": [ - { - "note": "================== COMMMANDS ========================" - }, - { - "name": "Say Hello from mlevison.GenAITaskChecker! (change this to your own command)", - "description": "Your first plugin!", - "jsFunction": "sayHello", - "alias": [ - "helloWorld Alias" - ], - "arguments": [ - "Append this text to document" - ] - }, - { - "NOTE": "DO NOT EDIT THIS COMMAND/TRIGGER", - "name": "GenAITaskChecker: Version", - "description": "Update + Check Version", - "jsFunction": "versionCheck" - }, - { - "description": "DO NOT EDIT THIS COMMAND/TRIGGER", - "name": "onOpen", - "jsFunction": "onOpen", - "hidden": true - }, - { - "description": "DO NOT EDIT THIS COMMAND/TRIGGER", - "name": "onEditorWillSave", - "jsFunction": "onEditorWillSave", - "hidden": true - }, - { - "NOTE": "DO NOT EDIT THIS COMMAND/TRIGGER", - "name": "onMessageFromHTMLView", - "description": "mlevison.GenAITaskChecker: Callback function to receive messages from HTML view", - "jsFunction": "onMessageFromHTMLView", - "hidden": true - }, - { - "NOTE": "DO NOT EDIT THIS COMMAND/TRIGGER", - "name": "GenAITaskChecker: Update Plugin Settings", - "description": "Preferences", - "jsFunction": "editSettings" - } - ], - "plugin.settings": [ - { - "note": "================== SETTINGS ========================" - }, - { - "COMMENT": "Plugin settings documentation: https://help.noteplan.co/article/123-plugin-configuration", - "type": "heading", - "title": "GenAITaskChecker Settings" - }, - { - "type": "hidden", - "key": "pluginID", - "default": "mlevison.GenAITaskChecker", - "COMMENT": "This is for use by the editSettings helper function. PluginID must match the plugin.id in the top of this file" - }, - { - "title": "A string in prefs", - "key": "settingsString", - "type": "string", - "description": "Enter some string and see it change when the plugin is run", - "default": "This default setting was set in plugin preferences!" - }, - { - "note": "================== DEBUGGING SETTINGS ========================" - }, - { - "NOTE": "DO NOT CHANGE THE FOLLOWING SETTINGS; ADD YOUR SETTINGS ABOVE ^^^", - "type": "separator" - }, - { - "type": "heading", - "title": "Debugging" - }, - { - "key": "_logLevel", - "type": "string", - "title": "Log Level", - "choices": [ - "DEBUG", - "INFO", - "WARN", - "ERROR", - "none" - ], - "description": "Set how much logging output will be displayed when executing GenAITaskChecker commands in NotePlan Plugin Console Logs (NotePlan -> Help -> Plugin Console)\n\n - DEBUG: Show All Logs\n - INFO: Only Show Info, Warnings, and Errors\n - WARN: Only Show Errors or Warnings\n - ERROR: Only Show Errors\n - none: Don't show any logs", - "default": "INFO", - "required": true - } - ] -} \ No newline at end of file diff --git a/mlevison.GenAITaskChecker/requiredFiles/html-plugin-comms.js b/mlevison.GenAITaskChecker/requiredFiles/html-plugin-comms.js deleted file mode 100644 index 90754ed28..000000000 --- a/mlevison.GenAITaskChecker/requiredFiles/html-plugin-comms.js +++ /dev/null @@ -1,106 +0,0 @@ -/** - * html-PluginComms.js - HTML Window: process data to/from the plugin - * This file is loaded by the browser via - - - - * 3) onMessageFromPlugin() below will receive your data from the plugin and route it to the appropriate function - * 4) Call sendMessageToPlugin(type:string, data:any) from your HTML window to send data back to the plugin asynchronously - */ - -/* eslint-disable no-undef */ -/* eslint-disable no-unused-vars */ - -/** - * onMessageFromPlugin is a router where you route the data returned from the plugin - * the plugin will send a 'type' and 'data' object - * this function is just a switch/router. Based on the type, call a function to process the data. - * do not do any processing here, just call the function to do the processing - * @param {string} type - * @param {any} data - */ -function onMessageFromPlugin(type, data) { - switch (type) { - case 'updateDiv': - onUpdateDivReceived(data) - break - default: - console.log(`onMessageFromPlugin: unknown type: ${type}`) - showError(`onMessageFromPlugin: received unknown type: ${type}`) - // ...call other functions to process the data for other types of messages from the plugin - } -} - -/**************************************************************************************************************************** - * DATA PROCESSING FUNCTIONS FOR RETURNED DATA FROM THE PLUGIN - ****************************************************************************************************************************/ -// these are the functions called in the onMessageFromPlugin function above - -/** - * Plugin wants to replace a div with some HTML (or plain text if innerText is true) - * @param { { divID: string, html: string, innerText:boolean } } data - */ -function onUpdateDivReceived(data) { - const { divID, html, innerText } = data - console.log(`onUpdateDivReceived: divID: ${divID}, html: ${html}`) - replaceHTML(divID, html, innerText) -} - -/**************************************************************************************************************************** - * EVENT HANDLERS FOR THE HTML VIEW - ****************************************************************************************************************************/ -// These event handlers are called by the HTML view when the user clicks on something -// It's a good idea to have a separate function for each event handler so that you can easily see what's going on -// And have the receiving function on the plugin side named the same thing as the event handler -// So it's easy to match them all up -// You could call sendMessageToPlugin directly from the HTML onClick event handler, but I prefer to have a separate function -// so you can do error checking, logging, etc. - -/** - * Event handler for the 'click' event on the status icon - * @param {string} filename - * @param {number} lineIndex - * @param {string} statusWas - */ -function onClickStatus(filename, lineIndex, statusWas, lineID) { - if (!filename || typeof lineIndex !== 'number' || !statusWas || !lineID) { - const msg = `onClickStatus: invalid data: filename: ${filename}, lineIndex: ${lineIndex}, statusWas: ${statusWas}, lineID: ${lineID}` - console.log(msg) - showError(msg) // you should have a div with id='error' in your HTML - } else { - console.log(`onClickStatus received click on: filename: ${filename}, lineIndex: ${lineIndex}, status: ${status}; sending 'onClickStatus' to plugin`) - const data = { filename, lineIndex: lineIndex, statusWas, lineID } - sendMessageToPlugin('onClickStatus', data) // actionName, data - } -} - -/**************************************************************************************************************************** - * HELPER FUNCTIONS - ****************************************************************************************************************************/ - -function replaceHTML(divID, html, innerText) { - console.log(`replaceHTML: divID: ${divID}, html: ${html}`) - const div = document.getElementById(divID) - if (div) { - if (innerText) { - div.innerText = html - } else { - div.innerHTML = html - } - } -} - -function showError(message) { - const div = document.getElementById('error') - if (div) { - div.innerText = message - } -} diff --git a/mlevison.GenAITaskChecker/src/NPMessagesFromHTMLWindow.js b/mlevison.GenAITaskChecker/src/NPMessagesFromHTMLWindow.js deleted file mode 100644 index 2f4739ab8..000000000 --- a/mlevison.GenAITaskChecker/src/NPMessagesFromHTMLWindow.js +++ /dev/null @@ -1,67 +0,0 @@ -// @flow -/** - * This file receives and processes messages from the HTML view - * You can ignore it if you are not going to use any HTML popup windows - * If you do want to use HTML windows, read the notes at the top of _requiredFiles/html-plugin-comms.js - * - * The function onClickStatus below is just an example of a function that could be called from the HTML view - */ - -import pluginJson from '../plugin.json' - -// ID needs match the custom id you pass when you open the window -const WINDOW_CUSTOM_ID = `${pluginJson['plugin.id']} HTML Window` // change if you want to use multiple html windows - -// import { getWindowIdFromCustomId } from '@helpers/NPWindows' -import { sendToHTMLWindow } from '@helpers/HTMLView' -import { getParagraphFromStaticObject } from '@helpers/NPParagraph' -import { log, logError, logDebug, timer, clo, JSP } from '@helpers/dev' - -/** - * Callback function to receive async messages from HTML view - * Plugin entrypoint for command: "/onMessageFromHTMLView" (called by plugin via sendMessageToHTMLView command) - * Do not do the processing in this function, but call a separate function to do the work. - * @author @dwertheimer - * @param {string} type - the type of action the HTML view wants the plugin to perform - * @param {any} data - the data that the HTML view sent to the plugin - */ -export function onMessageFromHTMLView(type: string, data: any): any { - try { - logDebug(pluginJson, `onMessageFromHTMLView running with args:${JSP(data)}`) - switch (type) { - case 'onClickStatus': - onClickStatus(data) // data is an array and could be multiple items. but in this case, we know we only need the first item which is an object - break - } - return {} // any function called by invoke... should return something (anything) to keep NP from reporting an error in the console - } catch (error) { - logError(pluginJson, JSP(error)) - } -} - -type ClickStatus = { filename: string, lineIndex: number, statusWas: string, lineID: string } - -/** - * Somebody clicked on a status icon in the HTML view - * (this is just a sample function called by the router - onMessageFromHTMLView() above) - * @param {ClickStatus} data - details of the item clicked - */ -export function onClickStatus(data: ClickStatus) { - const { filename, lineIndex, statusWas, lineID } = data - logDebug(pluginJson, `Plugin: onClickStatus running with statusWas:${statusWas}, filename:${filename}, lineIndex:${lineIndex}, statusWas:${statusWas}`) - const para = getParagraphFromStaticObject(data, ['filename', 'lineIndex']) - if (para) { - // you can do whatever you want here. For example, you could change the status of the paragraph - // to done depending on whether it was an open task or a checklist item - para.type = statusWas === 'open' ? 'done' : 'checklistDone' - para.note?.updateParagraph(para) - const newDivContent = `"${para.type}"Paragraph status was updated by the plugin!` - if (WINDOW_CUSTOM_ID) { - sendToHTMLWindow(WINDOW_CUSTOM_ID, 'updateDiv', { divID: lineID, html: newDivContent, innerText: false }) - } - // NOTE: in this particular case, it might have been easier to just call the refresh-page command, but I thought it worthwhile - // to show how to update a single div in the HTML view - } else { - logError(pluginJson, `onClickStatus: could not find paragraph for filename:${filename}, lineIndex:${lineIndex}`) - } -} diff --git a/mlevison.GenAITaskChecker/src/NPPluginMain.js b/mlevison.GenAITaskChecker/src/NPPluginMain.js deleted file mode 100644 index 7ed5ba5bf..000000000 --- a/mlevison.GenAITaskChecker/src/NPPluginMain.js +++ /dev/null @@ -1,75 +0,0 @@ -// @flow -// Plugin code goes in files like this. Can be one per command, or several in a file. -// `export async function [name of jsFunction called by Noteplan]` -// then include that function name as an export in the index.js file also -// About Flow: https://flow.org/en/docs/usage/#toc-write-flow-code -// Getting started with Flow in NotePlan: https://github.com/NotePlan/plugins/blob/main/Flow_Guide.md - -// NOTE: This file is named NPPluginMain.js (you could change that name and change the reference to it in index.js) -// As a matter of convention, we use NP at the beginning of files which contain calls to NotePlan APIs (Editor, DataStore, etc.) -// Because you cannot easily write tests for code that calls NotePlan APIs, we try to keep the code in the NP files as lean as possible -// and put the majority of the work in the /support folder files which have Jest tests for each function -// support/helpers is an example of a testable file that is used by the plugin command -// REMINDER, to build this plugin as you work on it: -// From the command line: -// `noteplan-cli plugin:dev mlevison.GenAITaskChecker --test --watch --coverage` -// IMPORTANT: It's a good idea for you to open the settings ASAP in NotePlan Preferences > Plugins and set your plugin's logging level to DEBUG - -/** - * LOGGING - * A user will be able to set their logging level in the plugin's settings (if you used the plugin:create command) - * As a general rule, you should use logDebug (see below) for messages while you're developing. As developer, - * you will set your log level in your plugin preferences to DEBUG and you will see these messages but - * an ordinary user will not. When you want to output a message,you can use the following. - * logging level commands for different levels of messages: - * - * logDebug(pluginJson,"Only developers or people helping debug will see these messages") - * log(pluginJson,"Ordinary users will see these informational messages") - * logWarn(pluginJson,"All users will see these warning/non-fatal messages") - * logError(pluginJson,"All users will see these fatal/error messages") - */ -import pluginJson from '../plugin.json' -import * as helpers from './support/helpers' -import { log, logDebug, logError, logWarn, clo, JSP } from '@helpers/dev' -import { createRunPluginCallbackUrl } from '@helpers/general' - -// NOTE: Plugin entrypoints (jsFunctions called by NotePlan) must be exported as async functions or you will get a TypeError in the NotePlan plugin console -// if you do not have an "await" statement inside your function, you can put an eslint-disable line like below so you don't get an error -// eslint-disable-next-line require-await -export async function sayHello(incoming: ?string = ''): Promise { - // every command/plugin entry point should always be wrapped in a try/catch block - try { - if (incoming?.length) { - // When commands are launched from NotePlan Command Bar, they are passed with no arguments - // if `incoming` is set, this plugin/command run must have come from a runPlugin call (e.g. clicking on a noteplan:// xcallback link or a template call) - Editor.insertTextAtCursor(`***You clicked the link!*** The message at the end of the link is "${incoming}". Now the rest of the plugin will run just as before...\n\n`) - } - - // a call to a support function in a separate file - const message = helpers.uppercase('Hello World from Test Plugin!') - - // this will appear in NotePlan Plugin Console (NotePlan > Help > Plugin Console) - log(pluginJson, `The plugin says: ${message}`) - - // Get some info from the plugin settings panel (where users can change settings) - const settings = DataStore.settings // Plugin settings documentation: https://help.noteplan.co/article/123-plugin-configuration - const settingsString = settings.settingsString ?? '' - // this will be inserted at cursor position in the Editor - Editor.insertTextAtCursor( - `${message}\n\nThis came from the Plugin Settings Panel: **"${settingsString}"** (You should go now to Preferences > Plugins, click the "cog" next to this plugin name and change the text. When you run this plugin again, you will see the new setting text.\n\n`, - ) - - // This will Console Log an Object that comes from the NotePlan API (in this case, the currently-open Note's paragraphs) - clo(Editor.note?.paragraphs, `The note paragraphs:`) - - if (!incoming?.length) { - // Create a XCallback URL that can run this command - const url = createRunPluginCallbackUrl(pluginJson['plugin.id'], pluginJson['plugin.commands'][0].name, ['This text was in the link!']) - Editor.insertTextAtCursor( - `This link could be used anywhere inside or outside of NotePlan to call this plugin:\n${url}\nGo ahead and click it! ^^^\nYou will see the results below:\n\n*****\n`, - ) - } - } catch (error) { - logError(pluginJson, JSP(error)) - } -} diff --git a/mlevison.GenAITaskChecker/src/NPTriggers-Hooks.js b/mlevison.GenAITaskChecker/src/NPTriggers-Hooks.js deleted file mode 100644 index 56f6defc7..000000000 --- a/mlevison.GenAITaskChecker/src/NPTriggers-Hooks.js +++ /dev/null @@ -1,119 +0,0 @@ -/* eslint-disable require-await */ -// @flow - -import pluginJson from '../plugin.json' // gives you access to the contents of plugin.json -import { log, logError, logDebug, timer, clo, JSP } from '@helpers/dev' -import { updateSettingData, pluginUpdated } from '@helpers/NPConfiguration' -import { showMessage } from '@helpers/userInput' - -/** - * NOTEPLAN PER-NOTE TRIGGERS - * - * The following functions are called by NotePlan automatically - * if a note has a triggers: section in its frontmatter - * See the documentation: https://help.noteplan.co/article/173-plugin-note-triggers - */ - -/** - * onOpen - * Plugin entrypoint for command: "/onOpen" - * Called when a note is opened and that note - * has a triggers: onOpen in its frontmatter - * @param {TNote} note - current note in Editor - */ -export async function onOpen(note: TNote): Promise { - try { - logDebug(pluginJson, `${pluginJson['plugin.id']} :: onOpen running for note:"${String(note.filename)}"`) - // Try to guard against infinite loops of opens/refreshing - // You can delete this code if you are sure that your onOpen trigger will not cause an infinite loop - // But the safest thing to do is put your code inside the if loop below to ensure it runs no more than once every 15s - const now = new Date() - if (Editor?.note?.changedDate) { - const lastEdit = new Date(Editor?.note?.changedDate) - if (now.getTime() - lastEdit.getTime() > 15000) { - logDebug(pluginJson, `onOpen ${timer(lastEdit)} since last edit`) - // Put your code here or call a function that does the work - } else { - logDebug(pluginJson, `onOpen: Only ${timer(lastEdit)} since last edit (hasn't been 15s)`) - } - } - } catch (error) { - logError(pluginJson, `onOpen: ${JSP(error)}`) - } -} - -/** - * onEditorWillSave - * Plugin entrypoint for command: "/onEditorWillSave" - */ -export async function onEditorWillSave() { - try { - logDebug(pluginJson, `${pluginJson['plugin.id']} :: onEditorWillSave running with note in Editor:"${String(Editor.filename)}"`) - // Put your code here or call a function that does the work - // Note: as stated in the documentation, if you want to change any content in the Editor - // before the file is written, you should NOT use the *note* variable here to change content - // Instead, use Editor.* commands (e.g. Editor.insertTextAtCursor()) or Editor.updateParagraphs() - } catch (error) { - logError(pluginJson, `onEditorWillSave: ${JSP(error)}`) - } -} - -/* - * NOTEPLAN GLOBAL PLUGIN HOOKS - * - * The rest of these functions are called by NotePlan automatically under certain conditions - * It is unlikely you will need to edit/add anything below this line - * - */ - -/** - * NotePlan calls this function after the plugin is installed or updated. - * The `updateSettingData` function looks through the new plugin settings in plugin.json and updates - * the user preferences to include any new fields - */ -export async function onUpdateOrInstall(): Promise { - try { - logDebug(pluginJson, `${pluginJson['plugin.id']} :: onUpdateOrInstall running`) - await updateSettingData(pluginJson) - await pluginUpdated(pluginJson, { code: 2, message: `Plugin Installed.` }) - } catch (error) { - logError(pluginJson, `onUpdateOrInstall: ${JSP(error)}`) - } -} - -/** - * NotePlan calls this function every time the plugin is run (any command in this plugin, including triggers) - * You should not need to edit this function. All work should be done in the commands themselves - */ -export function init(): void { - try { - logDebug(pluginJson, `${pluginJson['plugin.id']} :: init running`) - // clo(DataStore.settings, `${pluginJson['plugin.id']} Plugin Settings`) - DataStore.installOrUpdatePluginsByID([pluginJson['plugin.id']], false, false, false).then((r) => pluginUpdated(pluginJson, r)) - } catch (error) { - logError(pluginJson, `init: ${JSP(error)}`) - } -} - -/** - * NotePlan calls this function settings are updated in the Preferences panel - * You should not need to edit this function - */ -export async function onSettingsUpdated(): Promise { - try { - logDebug(pluginJson, `${pluginJson['plugin.id']} :: onSettingsUpdated running`) - } catch (error) { - logError(pluginJson, `onSettingsUpdated: ${JSP(error)}`) - } -} - -/** - * Check the version of the plugin (and force an update if the version is out of date) - */ -export async function versionCheck(): Promise { - try { - await showMessage(`Current Version: ${pluginJson['plugin.version']}`, 'OK', `${pluginJson['plugin.name']}`, true) - } catch (error) { - logError(pluginJson, JSP(error)) - } -} diff --git a/mlevison.GenAITaskChecker/src/index.js b/mlevison.GenAITaskChecker/src/index.js deleted file mode 100644 index baeed8c43..000000000 --- a/mlevison.GenAITaskChecker/src/index.js +++ /dev/null @@ -1,35 +0,0 @@ -// @flow -// Flow typing is important for reducing errors and improving the quality of the code. -// About Flow: https://flow.org/en/docs/usage/#toc-write-flow-code -// Getting started with Flow in NotePlan: https://github.com/NotePlan/plugins/blob/main/Flow_Guide.md -// Note: As you will see in this plugin folder, you can have multiple files -- e.g. one file per command or logical group of commands -// ...and separate files for helper/support functions that can be tested in isolation -// The `autowatch` packager combines them all into one script.js file for NotePlan to read -// From the command line: -// `noteplan-cli plugin:dev {{pluginId}} --test --watch --coverage` -// ...will watch for changes and will compile the Plugin script code -// and copy it to your plugins directory where NotePlan can find it -// Since NP reloads the Javascript every time you CMD-J to insert a plugin, -// you can immediately test the new code without restarting NotePlan -// This index.js file is where the packager starts looking for files to combine into one script.js file -// So you need to add a line below for each function that you want NP to have access to. -// Typically, listed below are only the top-level plug-in functions listed in plugin.json - -export { sayHello } from './NPPluginMain' // add one of these for every command specifified in plugin.json (the function could be in any file as long as it's exported) - -// FETCH mocking for offline testing -// If you want to use external server calls in your plugin, it can be useful to mock the server responses -// while you are developing the plugin. This allows you to test the plugin without having to -// have a server running or having to have a network connection (or wait/pay for the server calls) -// Comment the following import line out if you want to use live fetch/server endpoints (normal operation) -// Uncomment it for using server mocks (fake/canned responses) you define in support/fetchOverrides.js -// import './support/fetchOverrides' - -/** - * Other imports/exports - you will normally not need to edit these - */ -// eslint-disable-next-line import/order -export { editSettings } from '@helpers/NPSettings' -export { onUpdateOrInstall, init, onSettingsUpdated, versionCheck } from './NPTriggers-Hooks' -export { onOpen, onEditorWillSave } from './NPTriggers-Hooks' -export { onMessageFromHTMLView } from './NPMessagesFromHTMLWindow' diff --git a/mlevison.GenAITaskChecker/src/support/fetchOverrides.js b/mlevison.GenAITaskChecker/src/support/fetchOverrides.js deleted file mode 100644 index b0dd0bf57..000000000 --- a/mlevison.GenAITaskChecker/src/support/fetchOverrides.js +++ /dev/null @@ -1,57 +0,0 @@ -// @flow - -// This file is only loaded and fetch is overridden if the import is enabled in the index file - -/** - * FETCH MOCKING - * This file is used to override the fetch function (calls to an external server) with a fake response - * This allows you to test your plugin without having to have a server running or having to have a network connection - * or wait/pay for the server calls - * You can define your fake responses in this file or in a separate file (see below) - * ...and when your code makes a fetch call to a server, it will get (an appropriate) fake response instead - * You define the responses and the text that must be in the fetch call to yield a particular response - * (see the mockResponses array below) - */ - -/** - * 1) Import any of your fake responses that are saved as files here (or see below for defining them as strings) - * The file should contain the exact response that the live server would return - * You can save the response as a JSON file (like sampleFileResponse below) or as a string (like sampleTextResponse below) - */ -import sampleFileResponse from './fetchResponses/google.search-for-something.json' // a sample fake response saved as a JSON file - -// Other necessary imports -import { FetchMock, type FetchMockResponse } from '@mocks/Fetch.mock' -import { logDebug } from '@helpers/dev' - -/** - * 2) Or you can define your fake responses as strings in this file: - */ -// You could also just put all the fake responses here in this file -// A little messier, but if you don't have very many responses, or they are small/strings, it's fine -const sampleTextWeatherResponse = `Nuremberg: ☀️ +9°F` - -// 3) So the mock knows when to send back which response, you need to define the match and response for each mock response -// Fill in the match and response for each mock response you want to use -// The match object hast following properties: -// url: string - the url to match (can be a partial string, or can even be a string that includes regex) -// optionsBody: string - a partial string or string/regex included in the POST body of the request to match (sent in options.body to fetch) -// optionsBody is optional. If you don't need to match on the POST body (matching URL is enough), just leave it out -// The response MUST BE A STRING. So either use a string response (like sampleTextWeatherResponse above) or -// JSON.stringify your response object (like sampleFileResponse above) -const mockResponses: Array = [ - // the first mock below will match a POST request to google.com with the words "search for something" in the POST body - { match: { url: 'google.com', optionsBody: 'search for something' }, response: JSON.stringify(sampleFileResponse) }, - // the mock below will match any GET or POST request to "wttr.in/Nuremberg?format=3" regardless of the body - { match: { url: 'wttr.in/Nuremberg?format=3' }, response: sampleTextWeatherResponse }, -] - -/** - * DO NOT TOUCH ANYTHING BELOW THIS LINE - */ - -const fm = new FetchMock(mockResponses) // add one object to array for each mock response -fetch = async (url: string, opts: FetchOptions) => { - logDebug(`fetchOverrides.js`, `FetchMock faking response from: "${url}" (turn on/off in index.js)`) - return await fm.fetch(url, opts) -} //override the global fetch diff --git a/mlevison.GenAITaskChecker/src/support/fetchResponses/google.search-for-something.json b/mlevison.GenAITaskChecker/src/support/fetchResponses/google.search-for-something.json deleted file mode 100644 index 988c28a6c..000000000 --- a/mlevison.GenAITaskChecker/src/support/fetchResponses/google.search-for-something.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "someKey": "Some Value", - "youGet": "The Idea" -} \ No newline at end of file diff --git a/mlevison.GenAITaskChecker/src/support/helpers.js b/mlevison.GenAITaskChecker/src/support/helpers.js deleted file mode 100644 index ca321f04c..000000000 --- a/mlevison.GenAITaskChecker/src/support/helpers.js +++ /dev/null @@ -1,11 +0,0 @@ -// @flow -// Here's an example function that can be imported and used in the plugin code -// More importantly, this function is pure (no NotePlan API calls), which means it can be tested -// This is a good way to do much of your plugin work in isolation, with tests, and then the NPxxx files can be smaller -// And just focus on NotePlan input/output, with the majority of the work happening here -// Reminder: -// About Flow: https://flow.org/en/docs/usage/#toc-write-flow-code -// Getting started with Flow in NotePlan: https://github.com/NotePlan/plugins/blob/main/Flow_Guide.md -export function uppercase(str: string = ''): string { - return str.toUpperCase() -} From cc20cc43b40890a65e5f0b8b5b8d928b91652a4d Mon Sep 17 00:00:00 2001 From: Mark Levison Date: Mon, 21 Jul 2025 17:28:02 -0400 Subject: [PATCH 10/10] Update tasks.test..js Delete Console.log() calls --- helpers/__tests__/tasks.test..js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/helpers/__tests__/tasks.test..js b/helpers/__tests__/tasks.test..js index e657d9670..a3e2281f0 100644 --- a/helpers/__tests__/tasks.test..js +++ b/helpers/__tests__/tasks.test..js @@ -22,8 +22,6 @@ describe('mlevison.GenAITaskChecker', () => { test('prove Open and Cancelled Tasks found in sample note', () => { const foundTasks = sorting.getTasksByType(noteWIthOpenAndCancelledTasks.paragraphs) - console.log('foundTasks open', foundTasks.open, foundTasks.done, foundTasks.cancelled) - expect(foundTasks.open.length).toBe(1) expect(foundTasks.done.length).toBe(0) expect(foundTasks.cancelled.length).toBe(1) @@ -37,8 +35,6 @@ describe('mlevison.GenAITaskChecker', () => { test('prove Done Tasks found in sample note', () => { const foundTasks = sorting.getTasksByType(noteWIthDoneAndScheduledTasks.paragraphs) - console.log('foundTasks open', foundTasks.open, foundTasks.done, foundTasks.cancelled) - expect(foundTasks.open.length).toBe(0) expect(foundTasks.done.length).toBe(1) expect(foundTasks.cancelled.length).toBe(0)