diff --git a/credential_stubs/0g0kU1diEfzjmF4T.json b/credential_stubs/0g0kU1diEfzjmF4T.json new file mode 100644 index 0000000..06dd7dd --- /dev/null +++ b/credential_stubs/0g0kU1diEfzjmF4T.json @@ -0,0 +1,15 @@ +{ + "id": "0g0kU1diEfzjmF4T", + "name": "Jina AI account", + "type": "jinaAiApi", + "data": { + "apiKey": "" + }, + "ownedBy": { + "type": "personal", + "projectId": "FeLO36wNUAcn61Wj", + "projectName": "Ben W ", + "personalEmail": "admin@ben.io" + }, + "isGlobal": false +} \ No newline at end of file diff --git a/credential_stubs/4niF5T7sRQdREmPy.json b/credential_stubs/4niF5T7sRQdREmPy.json new file mode 100644 index 0000000..90f4105 --- /dev/null +++ b/credential_stubs/4niF5T7sRQdREmPy.json @@ -0,0 +1,15 @@ +{ + "id": "4niF5T7sRQdREmPy", + "name": "SerpAPI account", + "type": "serpApi", + "data": { + "apiKey": "" + }, + "ownedBy": { + "type": "personal", + "projectId": "FeLO36wNUAcn61Wj", + "projectName": "Ben W ", + "personalEmail": "admin@ben.io" + }, + "isGlobal": false +} \ No newline at end of file diff --git a/credential_stubs/5ENroY8sSuOXtF9l.json b/credential_stubs/5ENroY8sSuOXtF9l.json new file mode 100644 index 0000000..bb9fd44 --- /dev/null +++ b/credential_stubs/5ENroY8sSuOXtF9l.json @@ -0,0 +1,19 @@ +{ + "id": "5ENroY8sSuOXtF9l", + "name": "n8n-games", + "type": "postgres", + "data": { + "host": "", + "database": "", + "user": "", + "password": "", + "ssl": "" + }, + "ownedBy": { + "type": "personal", + "projectId": "FeLO36wNUAcn61Wj", + "projectName": "Ben W ", + "personalEmail": "admin@ben.io" + }, + "isGlobal": false +} \ No newline at end of file diff --git a/credential_stubs/7RU5tEhxEeWWpWU9.json b/credential_stubs/7RU5tEhxEeWWpWU9.json new file mode 100644 index 0000000..b15fa99 --- /dev/null +++ b/credential_stubs/7RU5tEhxEeWWpWU9.json @@ -0,0 +1,16 @@ +{ + "id": "7RU5tEhxEeWWpWU9", + "name": "ben.io-cal", + "type": "googleCalendarOAuth2Api", + "data": { + "clientId": "", + "clientSecret": "" + }, + "ownedBy": { + "type": "personal", + "projectId": "FeLO36wNUAcn61Wj", + "projectName": "Ben W ", + "personalEmail": "admin@ben.io" + }, + "isGlobal": false +} \ No newline at end of file diff --git a/credential_stubs/7XEtlKn9X5YzJfiJ.json b/credential_stubs/7XEtlKn9X5YzJfiJ.json new file mode 100644 index 0000000..34f669c --- /dev/null +++ b/credential_stubs/7XEtlKn9X5YzJfiJ.json @@ -0,0 +1,15 @@ +{ + "id": "7XEtlKn9X5YzJfiJ", + "name": "NPM Admin Credentials", + "type": "httpCustomAuth", + "data": { + "json": "" + }, + "ownedBy": { + "type": "personal", + "projectId": "FeLO36wNUAcn61Wj", + "projectName": "Ben W ", + "personalEmail": "admin@ben.io" + }, + "isGlobal": false +} \ No newline at end of file diff --git a/credential_stubs/7dAOuF30uXa32ly3.json b/credential_stubs/7dAOuF30uXa32ly3.json new file mode 100644 index 0000000..808ff32 --- /dev/null +++ b/credential_stubs/7dAOuF30uXa32ly3.json @@ -0,0 +1,16 @@ +{ + "id": "7dAOuF30uXa32ly3", + "name": "ollama local", + "type": "openAiApi", + "data": { + "apiKey": "", + "url": "" + }, + "ownedBy": { + "type": "personal", + "projectId": "FeLO36wNUAcn61Wj", + "projectName": "Ben W ", + "personalEmail": "admin@ben.io" + }, + "isGlobal": false +} \ No newline at end of file diff --git a/credential_stubs/9IAr5DfhRDKBMABQ.json b/credential_stubs/9IAr5DfhRDKBMABQ.json new file mode 100644 index 0000000..6ff68aa --- /dev/null +++ b/credential_stubs/9IAr5DfhRDKBMABQ.json @@ -0,0 +1,16 @@ +{ + "id": "9IAr5DfhRDKBMABQ", + "name": "Header Auth - Gitea", + "type": "httpHeaderAuth", + "data": { + "name": "", + "value": "" + }, + "ownedBy": { + "type": "personal", + "projectId": "FeLO36wNUAcn61Wj", + "projectName": "Ben W ", + "personalEmail": "admin@ben.io" + }, + "isGlobal": false +} \ No newline at end of file diff --git a/credential_stubs/9IvQHDCVzfNfB9hU.json b/credential_stubs/9IvQHDCVzfNfB9hU.json new file mode 100644 index 0000000..7b31251 --- /dev/null +++ b/credential_stubs/9IvQHDCVzfNfB9hU.json @@ -0,0 +1,15 @@ +{ + "id": "9IvQHDCVzfNfB9hU", + "name": "base-1-flash-only", + "type": "googlePalmApi", + "data": { + "apiKey": "" + }, + "ownedBy": { + "type": "personal", + "projectId": "FeLO36wNUAcn61Wj", + "projectName": "Ben W ", + "personalEmail": "admin@ben.io" + }, + "isGlobal": false +} \ No newline at end of file diff --git a/credential_stubs/9UFJNgYndzFIHJAf.json b/credential_stubs/9UFJNgYndzFIHJAf.json new file mode 100644 index 0000000..e41443a --- /dev/null +++ b/credential_stubs/9UFJNgYndzFIHJAf.json @@ -0,0 +1,16 @@ +{ + "id": "9UFJNgYndzFIHJAf", + "name": "Nano-GPT", + "type": "openAiApi", + "data": { + "apiKey": "", + "url": "" + }, + "ownedBy": { + "type": "personal", + "projectId": "FeLO36wNUAcn61Wj", + "projectName": "Ben W ", + "personalEmail": "admin@ben.io" + }, + "isGlobal": false +} \ No newline at end of file diff --git a/credential_stubs/9grzZwW7Br6SzdV8.json b/credential_stubs/9grzZwW7Br6SzdV8.json new file mode 100644 index 0000000..55b9c20 --- /dev/null +++ b/credential_stubs/9grzZwW7Br6SzdV8.json @@ -0,0 +1,19 @@ +{ + "id": "9grzZwW7Br6SzdV8", + "name": "n8n-media", + "type": "postgres", + "data": { + "host": "", + "database": "", + "user": "", + "password": "", + "ssl": "" + }, + "ownedBy": { + "type": "personal", + "projectId": "FeLO36wNUAcn61Wj", + "projectName": "Ben W ", + "personalEmail": "admin@ben.io" + }, + "isGlobal": false +} \ No newline at end of file diff --git a/credential_stubs/A66wBBekagQkUFzd.json b/credential_stubs/A66wBBekagQkUFzd.json new file mode 100644 index 0000000..99649ac --- /dev/null +++ b/credential_stubs/A66wBBekagQkUFzd.json @@ -0,0 +1,16 @@ +{ + "id": "A66wBBekagQkUFzd", + "name": "bhw-gmail", + "type": "gmailOAuth2", + "data": { + "clientId": "", + "clientSecret": "" + }, + "ownedBy": { + "type": "personal", + "projectId": "FeLO36wNUAcn61Wj", + "projectName": "Ben W ", + "personalEmail": "admin@ben.io" + }, + "isGlobal": false +} \ No newline at end of file diff --git a/credential_stubs/BRiFacyi0A60Y7ZZ.json b/credential_stubs/BRiFacyi0A60Y7ZZ.json new file mode 100644 index 0000000..36dd591 --- /dev/null +++ b/credential_stubs/BRiFacyi0A60Y7ZZ.json @@ -0,0 +1,16 @@ +{ + "id": "BRiFacyi0A60Y7ZZ", + "name": "mam_id", + "type": "httpBasicAuth", + "data": { + "user": "", + "password": "" + }, + "ownedBy": { + "type": "personal", + "projectId": "FeLO36wNUAcn61Wj", + "projectName": "Ben W ", + "personalEmail": "admin@ben.io" + }, + "isGlobal": false +} \ No newline at end of file diff --git a/credential_stubs/ERgv4rlSLgq8fsK9.json b/credential_stubs/ERgv4rlSLgq8fsK9.json new file mode 100644 index 0000000..72857a4 --- /dev/null +++ b/credential_stubs/ERgv4rlSLgq8fsK9.json @@ -0,0 +1,15 @@ +{ + "id": "ERgv4rlSLgq8fsK9", + "name": "OpenRouter account", + "type": "openRouterApi", + "data": { + "apiKey": "" + }, + "ownedBy": { + "type": "personal", + "projectId": "FeLO36wNUAcn61Wj", + "projectName": "Ben W ", + "personalEmail": "admin@ben.io" + }, + "isGlobal": false +} \ No newline at end of file diff --git a/credential_stubs/ErcYIjq8Xs0GRaPF.json b/credential_stubs/ErcYIjq8Xs0GRaPF.json new file mode 100644 index 0000000..feab933 --- /dev/null +++ b/credential_stubs/ErcYIjq8Xs0GRaPF.json @@ -0,0 +1,16 @@ +{ + "id": "ErcYIjq8Xs0GRaPF", + "name": "ben.io-gtasks", + "type": "googleTasksOAuth2Api", + "data": { + "clientId": "", + "clientSecret": "" + }, + "ownedBy": { + "type": "personal", + "projectId": "FeLO36wNUAcn61Wj", + "projectName": "Ben W ", + "personalEmail": "admin@ben.io" + }, + "isGlobal": false +} \ No newline at end of file diff --git a/credential_stubs/G8eA8XeS9P5axwJd.json b/credential_stubs/G8eA8XeS9P5axwJd.json new file mode 100644 index 0000000..9a7253b --- /dev/null +++ b/credential_stubs/G8eA8XeS9P5axwJd.json @@ -0,0 +1,16 @@ +{ + "id": "G8eA8XeS9P5axwJd", + "name": "mam cookie auth header", + "type": "httpHeaderAuth", + "data": { + "name": "", + "value": "" + }, + "ownedBy": { + "type": "personal", + "projectId": "FeLO36wNUAcn61Wj", + "projectName": "Ben W ", + "personalEmail": "admin@ben.io" + }, + "isGlobal": true +} \ No newline at end of file diff --git a/credential_stubs/GDcJYxgCLDRHH8m7.json b/credential_stubs/GDcJYxgCLDRHH8m7.json new file mode 100644 index 0000000..4fd249d --- /dev/null +++ b/credential_stubs/GDcJYxgCLDRHH8m7.json @@ -0,0 +1,16 @@ +{ + "id": "GDcJYxgCLDRHH8m7", + "name": "bhw2249-gsheets", + "type": "googleSheetsOAuth2Api", + "data": { + "clientId": "", + "clientSecret": "" + }, + "ownedBy": { + "type": "personal", + "projectId": "FeLO36wNUAcn61Wj", + "projectName": "Ben W ", + "personalEmail": "admin@ben.io" + }, + "isGlobal": false +} \ No newline at end of file diff --git a/credential_stubs/Ik8CFyap8ic2Md3M.json b/credential_stubs/Ik8CFyap8ic2Md3M.json new file mode 100644 index 0000000..d32e7e3 --- /dev/null +++ b/credential_stubs/Ik8CFyap8ic2Md3M.json @@ -0,0 +1,19 @@ +{ + "id": "Ik8CFyap8ic2Md3M", + "name": "n8n-infra", + "type": "postgres", + "data": { + "host": "", + "database": "", + "user": "", + "password": "", + "ssl": "" + }, + "ownedBy": { + "type": "personal", + "projectId": "FeLO36wNUAcn61Wj", + "projectName": "Ben W ", + "personalEmail": "admin@ben.io" + }, + "isGlobal": false +} \ No newline at end of file diff --git a/credential_stubs/N7YEwAQv26RHwoLx.json b/credential_stubs/N7YEwAQv26RHwoLx.json new file mode 100644 index 0000000..e4a1063 --- /dev/null +++ b/credential_stubs/N7YEwAQv26RHwoLx.json @@ -0,0 +1,18 @@ +{ + "id": "N7YEwAQv26RHwoLx", + "name": "SMTP account", + "type": "smtp", + "data": { + "user": "", + "password": "", + "host": "", + "hostName": "" + }, + "ownedBy": { + "type": "personal", + "projectId": "FeLO36wNUAcn61Wj", + "projectName": "Ben W ", + "personalEmail": "admin@ben.io" + }, + "isGlobal": false +} \ No newline at end of file diff --git a/credential_stubs/QRBx9RMx4KoFwGgl.json b/credential_stubs/QRBx9RMx4KoFwGgl.json new file mode 100644 index 0000000..71cba17 --- /dev/null +++ b/credential_stubs/QRBx9RMx4KoFwGgl.json @@ -0,0 +1,16 @@ +{ + "id": "QRBx9RMx4KoFwGgl", + "name": "Nvidia account", + "type": "openAiApi", + "data": { + "apiKey": "", + "url": "" + }, + "ownedBy": { + "type": "personal", + "projectId": "FeLO36wNUAcn61Wj", + "projectName": "Ben W ", + "personalEmail": "admin@ben.io" + }, + "isGlobal": false +} \ No newline at end of file diff --git a/credential_stubs/S2dcVMjrpg0I0kdV.json b/credential_stubs/S2dcVMjrpg0I0kdV.json new file mode 100644 index 0000000..f97ea53 --- /dev/null +++ b/credential_stubs/S2dcVMjrpg0I0kdV.json @@ -0,0 +1,18 @@ +{ + "id": "S2dcVMjrpg0I0kdV", + "name": "vscode-dev.local.ben.io", + "type": "sshPrivateKey", + "data": { + "host": "", + "username": "", + "privateKey": "", + "passphrase": "" + }, + "ownedBy": { + "type": "personal", + "projectId": "FeLO36wNUAcn61Wj", + "projectName": "Ben W ", + "personalEmail": "admin@ben.io" + }, + "isGlobal": false +} \ No newline at end of file diff --git a/credential_stubs/SItxcw7hDLHB7DYY.json b/credential_stubs/SItxcw7hDLHB7DYY.json new file mode 100644 index 0000000..f753435 --- /dev/null +++ b/credential_stubs/SItxcw7hDLHB7DYY.json @@ -0,0 +1,16 @@ +{ + "id": "SItxcw7hDLHB7DYY", + "name": "Chutes AI", + "type": "openAiApi", + "data": { + "apiKey": "", + "url": "" + }, + "ownedBy": { + "type": "personal", + "projectId": "FeLO36wNUAcn61Wj", + "projectName": "Ben W ", + "personalEmail": "admin@ben.io" + }, + "isGlobal": false +} \ No newline at end of file diff --git a/credential_stubs/TE5lVqfBn06ypxIU.json b/credential_stubs/TE5lVqfBn06ypxIU.json new file mode 100644 index 0000000..60d51ad --- /dev/null +++ b/credential_stubs/TE5lVqfBn06ypxIU.json @@ -0,0 +1,16 @@ +{ + "id": "TE5lVqfBn06ypxIU", + "name": "cerebras.ai", + "type": "openAiApi", + "data": { + "apiKey": "", + "url": "" + }, + "ownedBy": { + "type": "personal", + "projectId": "FeLO36wNUAcn61Wj", + "projectName": "Ben W ", + "personalEmail": "admin@ben.io" + }, + "isGlobal": false +} \ No newline at end of file diff --git a/credential_stubs/VHbUFo39yKkrSroG.json b/credential_stubs/VHbUFo39yKkrSroG.json new file mode 100644 index 0000000..9cf0fca --- /dev/null +++ b/credential_stubs/VHbUFo39yKkrSroG.json @@ -0,0 +1,16 @@ +{ + "id": "VHbUFo39yKkrSroG", + "name": "ben.io-gmail", + "type": "gmailOAuth2", + "data": { + "clientId": "", + "clientSecret": "" + }, + "ownedBy": { + "type": "personal", + "projectId": "FeLO36wNUAcn61Wj", + "projectName": "Ben W ", + "personalEmail": "admin@ben.io" + }, + "isGlobal": false +} \ No newline at end of file diff --git a/credential_stubs/X0JzSAfmjzASQVau.json b/credential_stubs/X0JzSAfmjzASQVau.json new file mode 100644 index 0000000..688c49b --- /dev/null +++ b/credential_stubs/X0JzSAfmjzASQVau.json @@ -0,0 +1,15 @@ +{ + "id": "X0JzSAfmjzASQVau", + "name": "supa base custom auth", + "type": "httpCustomAuth", + "data": { + "json": "" + }, + "ownedBy": { + "type": "personal", + "projectId": "FeLO36wNUAcn61Wj", + "projectName": "Ben W ", + "personalEmail": "admin@ben.io" + }, + "isGlobal": false +} \ No newline at end of file diff --git a/credential_stubs/YMRbpffqZG7PHJb6.json b/credential_stubs/YMRbpffqZG7PHJb6.json new file mode 100644 index 0000000..5d8a859 --- /dev/null +++ b/credential_stubs/YMRbpffqZG7PHJb6.json @@ -0,0 +1,15 @@ +{ + "id": "YMRbpffqZG7PHJb6", + "name": "Google Gemini(PaLM) Api account", + "type": "googlePalmApi", + "data": { + "apiKey": "" + }, + "ownedBy": { + "type": "personal", + "projectId": "FeLO36wNUAcn61Wj", + "projectName": "Ben W ", + "personalEmail": "admin@ben.io" + }, + "isGlobal": false +} \ No newline at end of file diff --git a/credential_stubs/Z7DyxPOpTq3ClEPl.json b/credential_stubs/Z7DyxPOpTq3ClEPl.json new file mode 100644 index 0000000..dc2684c --- /dev/null +++ b/credential_stubs/Z7DyxPOpTq3ClEPl.json @@ -0,0 +1,16 @@ +{ + "id": "Z7DyxPOpTq3ClEPl", + "name": "n8n account", + "type": "n8nApi", + "data": { + "apiKey": "", + "baseUrl": "" + }, + "ownedBy": { + "type": "personal", + "projectId": "FeLO36wNUAcn61Wj", + "projectName": "Ben W ", + "personalEmail": "admin@ben.io" + }, + "isGlobal": false +} \ No newline at end of file diff --git a/credential_stubs/dsnKfvOBMkgU21Lt.json b/credential_stubs/dsnKfvOBMkgU21Lt.json new file mode 100644 index 0000000..6311a8a --- /dev/null +++ b/credential_stubs/dsnKfvOBMkgU21Lt.json @@ -0,0 +1,16 @@ +{ + "id": "dsnKfvOBMkgU21Lt", + "name": "supabase postgres account", + "type": "postgres", + "data": { + "host": "", + "password": "" + }, + "ownedBy": { + "type": "personal", + "projectId": "FeLO36wNUAcn61Wj", + "projectName": "Ben W ", + "personalEmail": "admin@ben.io" + }, + "isGlobal": false +} \ No newline at end of file diff --git a/credential_stubs/eKd8zVYf3jN5n9hq.json b/credential_stubs/eKd8zVYf3jN5n9hq.json new file mode 100644 index 0000000..7e38a44 --- /dev/null +++ b/credential_stubs/eKd8zVYf3jN5n9hq.json @@ -0,0 +1,16 @@ +{ + "id": "eKd8zVYf3jN5n9hq", + "name": "bhw-gcal", + "type": "googleCalendarOAuth2Api", + "data": { + "clientId": "", + "clientSecret": "" + }, + "ownedBy": { + "type": "personal", + "projectId": "FeLO36wNUAcn61Wj", + "projectName": "Ben W ", + "personalEmail": "admin@ben.io" + }, + "isGlobal": false +} \ No newline at end of file diff --git a/credential_stubs/foyggnWz8Sv0OO5w.json b/credential_stubs/foyggnWz8Sv0OO5w.json new file mode 100644 index 0000000..87c7160 --- /dev/null +++ b/credential_stubs/foyggnWz8Sv0OO5w.json @@ -0,0 +1,18 @@ +{ + "id": "foyggnWz8Sv0OO5w", + "name": "seed-1.dfw.ben.io", + "type": "sshPrivateKey", + "data": { + "host": "", + "port": 29000, + "username": "", + "privateKey": "" + }, + "ownedBy": { + "type": "personal", + "projectId": "FeLO36wNUAcn61Wj", + "projectName": "Ben W ", + "personalEmail": "admin@ben.io" + }, + "isGlobal": false +} \ No newline at end of file diff --git a/credential_stubs/gsbVEcmxwWZIB4ef.json b/credential_stubs/gsbVEcmxwWZIB4ef.json new file mode 100644 index 0000000..e3653ab --- /dev/null +++ b/credential_stubs/gsbVEcmxwWZIB4ef.json @@ -0,0 +1,18 @@ +{ + "id": "gsbVEcmxwWZIB4ef", + "name": "IMAP account", + "type": "imap", + "data": { + "user": "", + "password": "", + "host": "", + "allowUnauthorizedCerts": true + }, + "ownedBy": { + "type": "personal", + "projectId": "FeLO36wNUAcn61Wj", + "projectName": "Ben W ", + "personalEmail": "admin@ben.io" + }, + "isGlobal": false +} \ No newline at end of file diff --git a/credential_stubs/iymUPilnVhfL3h5D.json b/credential_stubs/iymUPilnVhfL3h5D.json new file mode 100644 index 0000000..2b0b55a --- /dev/null +++ b/credential_stubs/iymUPilnVhfL3h5D.json @@ -0,0 +1,16 @@ +{ + "id": "iymUPilnVhfL3h5D", + "name": "transmission", + "type": "httpBasicAuth", + "data": { + "user": "", + "password": "" + }, + "ownedBy": { + "type": "personal", + "projectId": "FeLO36wNUAcn61Wj", + "projectName": "Ben W ", + "personalEmail": "admin@ben.io" + }, + "isGlobal": false +} \ No newline at end of file diff --git a/credential_stubs/j03eT0qxJpv7H5an.json b/credential_stubs/j03eT0qxJpv7H5an.json new file mode 100644 index 0000000..b71c055 --- /dev/null +++ b/credential_stubs/j03eT0qxJpv7H5an.json @@ -0,0 +1,16 @@ +{ + "id": "j03eT0qxJpv7H5an", + "name": "Zabbix account", + "type": "zabbixApi", + "data": { + "url": "", + "apiToken": "" + }, + "ownedBy": { + "type": "personal", + "projectId": "FeLO36wNUAcn61Wj", + "projectName": "Ben W ", + "personalEmail": "admin@ben.io" + }, + "isGlobal": false +} \ No newline at end of file diff --git a/credential_stubs/jBV4AQlsHxWnFXsp.json b/credential_stubs/jBV4AQlsHxWnFXsp.json new file mode 100644 index 0000000..e3002b6 --- /dev/null +++ b/credential_stubs/jBV4AQlsHxWnFXsp.json @@ -0,0 +1,17 @@ +{ + "id": "jBV4AQlsHxWnFXsp", + "name": "homepage.sshkey", + "type": "sshPrivateKey", + "data": { + "host": "", + "username": "", + "privateKey": "" + }, + "ownedBy": { + "type": "personal", + "projectId": "FeLO36wNUAcn61Wj", + "projectName": "Ben W ", + "personalEmail": "admin@ben.io" + }, + "isGlobal": false +} \ No newline at end of file diff --git a/credential_stubs/l1g3pgQImkg18AzR.json b/credential_stubs/l1g3pgQImkg18AzR.json new file mode 100644 index 0000000..b4800dc --- /dev/null +++ b/credential_stubs/l1g3pgQImkg18AzR.json @@ -0,0 +1,15 @@ +{ + "id": "l1g3pgQImkg18AzR", + "name": "Ollama account", + "type": "ollamaApi", + "data": { + "baseUrl": "" + }, + "ownedBy": { + "type": "personal", + "projectId": "FeLO36wNUAcn61Wj", + "projectName": "Ben W ", + "personalEmail": "admin@ben.io" + }, + "isGlobal": false +} \ No newline at end of file diff --git a/credential_stubs/lWyf2ikOGHTTwnSU.json b/credential_stubs/lWyf2ikOGHTTwnSU.json new file mode 100644 index 0000000..8292b71 --- /dev/null +++ b/credential_stubs/lWyf2ikOGHTTwnSU.json @@ -0,0 +1,16 @@ +{ + "id": "lWyf2ikOGHTTwnSU", + "name": "Supabase account", + "type": "supabaseApi", + "data": { + "host": "", + "serviceRole": "" + }, + "ownedBy": { + "type": "personal", + "projectId": "FeLO36wNUAcn61Wj", + "projectName": "Ben W ", + "personalEmail": "admin@ben.io" + }, + "isGlobal": false +} \ No newline at end of file diff --git a/credential_stubs/nzr2GDHOdD1PjK08.json b/credential_stubs/nzr2GDHOdD1PjK08.json new file mode 100644 index 0000000..d54c72a --- /dev/null +++ b/credential_stubs/nzr2GDHOdD1PjK08.json @@ -0,0 +1,16 @@ +{ + "id": "nzr2GDHOdD1PjK08", + "name": "Gitea account", + "type": "giteaApi", + "data": { + "url": "", + "accessToken": "" + }, + "ownedBy": { + "type": "personal", + "projectId": "FeLO36wNUAcn61Wj", + "projectName": "Ben W ", + "personalEmail": "admin@ben.io" + }, + "isGlobal": false +} \ No newline at end of file diff --git a/credential_stubs/o6A594KZAPzN3LTR.json b/credential_stubs/o6A594KZAPzN3LTR.json new file mode 100644 index 0000000..0b19daf --- /dev/null +++ b/credential_stubs/o6A594KZAPzN3LTR.json @@ -0,0 +1,15 @@ +{ + "id": "o6A594KZAPzN3LTR", + "name": "Brave Search account", + "type": "braveSearchApi", + "data": { + "apiKey": "" + }, + "ownedBy": { + "type": "personal", + "projectId": "FeLO36wNUAcn61Wj", + "projectName": "Ben W ", + "personalEmail": "admin@ben.io" + }, + "isGlobal": false +} \ No newline at end of file diff --git a/credential_stubs/qQNqeSAtYrG6Vi5N.json b/credential_stubs/qQNqeSAtYrG6Vi5N.json new file mode 100644 index 0000000..f2915f1 --- /dev/null +++ b/credential_stubs/qQNqeSAtYrG6Vi5N.json @@ -0,0 +1,16 @@ +{ + "id": "qQNqeSAtYrG6Vi5N", + "name": "ben.io-gsheet", + "type": "googleSheetsOAuth2Api", + "data": { + "clientId": "", + "clientSecret": "" + }, + "ownedBy": { + "type": "personal", + "projectId": "FeLO36wNUAcn61Wj", + "projectName": "Ben W ", + "personalEmail": "admin@ben.io" + }, + "isGlobal": false +} \ No newline at end of file diff --git a/credential_stubs/sxQYMPEq6GLbc9Av.json b/credential_stubs/sxQYMPEq6GLbc9Av.json new file mode 100644 index 0000000..ffd93d8 --- /dev/null +++ b/credential_stubs/sxQYMPEq6GLbc9Av.json @@ -0,0 +1,16 @@ +{ + "id": "sxQYMPEq6GLbc9Av", + "name": "n8n_auth_cookie", + "type": "httpHeaderAuth", + "data": { + "name": "", + "value": "" + }, + "ownedBy": { + "type": "personal", + "projectId": "FeLO36wNUAcn61Wj", + "projectName": "Ben W ", + "personalEmail": "admin@ben.io" + }, + "isGlobal": false +} \ No newline at end of file diff --git a/credential_stubs/sxSUdecXdMfKPuTu.json b/credential_stubs/sxSUdecXdMfKPuTu.json new file mode 100644 index 0000000..16583aa --- /dev/null +++ b/credential_stubs/sxSUdecXdMfKPuTu.json @@ -0,0 +1,16 @@ +{ + "id": "sxSUdecXdMfKPuTu", + "name": "llm-proxy.ben.io", + "type": "openAiApi", + "data": { + "apiKey": "", + "url": "" + }, + "ownedBy": { + "type": "personal", + "projectId": "FeLO36wNUAcn61Wj", + "projectName": "Ben W ", + "personalEmail": "admin@ben.io" + }, + "isGlobal": false +} \ No newline at end of file diff --git a/credential_stubs/tcNjvEki2DGt40hn.json b/credential_stubs/tcNjvEki2DGt40hn.json new file mode 100644 index 0000000..b720fcc --- /dev/null +++ b/credential_stubs/tcNjvEki2DGt40hn.json @@ -0,0 +1,16 @@ +{ + "id": "tcNjvEki2DGt40hn", + "name": "bhw-gtasks", + "type": "googleTasksOAuth2Api", + "data": { + "clientId": "", + "clientSecret": "" + }, + "ownedBy": { + "type": "personal", + "projectId": "FeLO36wNUAcn61Wj", + "projectName": "Ben W ", + "personalEmail": "admin@ben.io" + }, + "isGlobal": false +} \ No newline at end of file diff --git a/credential_stubs/uYjjcswcBGrqXgpk.json b/credential_stubs/uYjjcswcBGrqXgpk.json new file mode 100644 index 0000000..244bc17 --- /dev/null +++ b/credential_stubs/uYjjcswcBGrqXgpk.json @@ -0,0 +1,19 @@ +{ + "id": "uYjjcswcBGrqXgpk", + "name": "openrouter", + "type": "openAiApi", + "data": { + "apiKey": "", + "url": "", + "header": true, + "headerName": "", + "headerValue": "" + }, + "ownedBy": { + "type": "personal", + "projectId": "FeLO36wNUAcn61Wj", + "projectName": "Ben W ", + "personalEmail": "admin@ben.io" + }, + "isGlobal": false +} \ No newline at end of file diff --git a/credential_stubs/vOzhicmbOwx1XDF8.json b/credential_stubs/vOzhicmbOwx1XDF8.json new file mode 100644 index 0000000..46c22ed --- /dev/null +++ b/credential_stubs/vOzhicmbOwx1XDF8.json @@ -0,0 +1,17 @@ +{ + "id": "vOzhicmbOwx1XDF8", + "name": "seed-0.local.ben.io", + "type": "sshPrivateKey", + "data": { + "host": "", + "username": "", + "privateKey": "" + }, + "ownedBy": { + "type": "personal", + "projectId": "FeLO36wNUAcn61Wj", + "projectName": "Ben W ", + "personalEmail": "admin@ben.io" + }, + "isGlobal": false +} \ No newline at end of file diff --git a/credential_stubs/y1IVnGq2Wiqy3HBf.json b/credential_stubs/y1IVnGq2Wiqy3HBf.json new file mode 100644 index 0000000..a4cd24b --- /dev/null +++ b/credential_stubs/y1IVnGq2Wiqy3HBf.json @@ -0,0 +1,15 @@ +{ + "id": "y1IVnGq2Wiqy3HBf", + "name": "Tavily account", + "type": "tavilyApi", + "data": { + "apiKey": "" + }, + "ownedBy": { + "type": "personal", + "projectId": "FeLO36wNUAcn61Wj", + "projectName": "Ben W ", + "personalEmail": "admin@ben.io" + }, + "isGlobal": false +} \ No newline at end of file diff --git a/folders.json b/folders.json new file mode 100644 index 0000000..8b2bb5b --- /dev/null +++ b/folders.json @@ -0,0 +1,60 @@ +{ + "folders": [ + { + "id": "of8yoeyjjIAhYdnQ", + "name": "core", + "parentFolderId": null, + "homeProjectId": "FeLO36wNUAcn61Wj", + "createdAt": "2025-10-13T01:30:54.855Z", + "updatedAt": "2025-10-13T01:30:54.855Z" + }, + { + "id": "eWW72giJDI4fxlWw", + "name": "NPM", + "parentFolderId": null, + "homeProjectId": "FeLO36wNUAcn61Wj", + "createdAt": "2025-10-20T03:42:27.697Z", + "updatedAt": "2025-10-20T03:42:27.697Z" + }, + { + "id": "kUg4HIPXraph3M0E", + "name": "Audiobooks", + "parentFolderId": null, + "homeProjectId": "FeLO36wNUAcn61Wj", + "createdAt": "2025-11-02T14:20:53.630Z", + "updatedAt": "2025-11-02T14:20:53.630Z" + }, + { + "id": "6tDyZCwqELStb6Ik", + "name": "MAM", + "parentFolderId": null, + "homeProjectId": "FeLO36wNUAcn61Wj", + "createdAt": "2025-12-09T03:50:09.700Z", + "updatedAt": "2025-12-09T03:50:09.700Z" + }, + { + "id": "OJ2UfPNUOAOHlllh", + "name": "Personal Dev", + "parentFolderId": null, + "homeProjectId": "FeLO36wNUAcn61Wj", + "createdAt": "2025-12-17T14:15:10.406Z", + "updatedAt": "2025-12-17T14:15:10.406Z" + }, + { + "id": "HWgaFb7kLF649L7l", + "name": "EVE", + "parentFolderId": null, + "homeProjectId": "FeLO36wNUAcn61Wj", + "createdAt": "2025-12-21T15:55:12.366Z", + "updatedAt": "2025-12-21T15:55:12.366Z" + }, + { + "id": "LTWZD96boqxk9sIs", + "name": "Home Lab", + "parentFolderId": null, + "homeProjectId": "FeLO36wNUAcn61Wj", + "createdAt": "2025-12-21T15:55:27.361Z", + "updatedAt": "2025-12-21T15:55:27.361Z" + } + ] +} \ No newline at end of file diff --git a/tags.json b/tags.json new file mode 100644 index 0000000..a1f17c1 --- /dev/null +++ b/tags.json @@ -0,0 +1,66 @@ +{ + "tags": [ + { + "id": "FydpKYmttDwoZVAA", + "name": "n8n-media" + }, + { + "id": "zrmVqhwdDmkuhhaQ", + "name": "ai_tool" + }, + { + "id": "ct0Rtzpu15B497av", + "name": "ai_agent" + } + ], + "mappings": [ + { + "workflowId": "VUwFjFF2UhNout2T", + "tagId": "zrmVqhwdDmkuhhaQ" + }, + { + "workflowId": "0gxdxCdYQ7oXk7gC", + "tagId": "FydpKYmttDwoZVAA" + }, + { + "workflowId": "v3KQi4UoMlhH7JIW", + "tagId": "FydpKYmttDwoZVAA" + }, + { + "workflowId": "f2rn29FKq1ejX2ax", + "tagId": "zrmVqhwdDmkuhhaQ" + }, + { + "workflowId": "Mdopqz1Tq0OHDFq1", + "tagId": "ct0Rtzpu15B497av" + }, + { + "workflowId": "Mdopqz1Tq0OHDFq1", + "tagId": "zrmVqhwdDmkuhhaQ" + }, + { + "workflowId": "7kAZyLHOpYKg4riN", + "tagId": "ct0Rtzpu15B497av" + }, + { + "workflowId": "cPWZKfrHOUSUZjIp", + "tagId": "FydpKYmttDwoZVAA" + }, + { + "workflowId": "Z_YHsJaf_pyFQR6e7VuLo", + "tagId": "zrmVqhwdDmkuhhaQ" + }, + { + "workflowId": "J3uKCCbSuQ1fdJkC", + "tagId": "FydpKYmttDwoZVAA" + }, + { + "workflowId": "H6TZCHyiYOr1X6Xf", + "tagId": "FydpKYmttDwoZVAA" + }, + { + "workflowId": "c3N3bYrOAy0rNGGq", + "tagId": "zrmVqhwdDmkuhhaQ" + } + ] +} \ No newline at end of file diff --git a/workflows/0gxdxCdYQ7oXk7gC.json b/workflows/0gxdxCdYQ7oXk7gC.json new file mode 100644 index 0000000..47ca721 --- /dev/null +++ b/workflows/0gxdxCdYQ7oXk7gC.json @@ -0,0 +1,378 @@ +{ + "id": "0gxdxCdYQ7oXk7gC", + "name": "MAM Series Enricher", + "nodes": [ + { + "parameters": {}, + "id": "execute-workflow-trigger", + "name": "Execute Workflow Trigger", + "position": [ + 224, + 640 + ], + "type": "n8n-nodes-base.executeWorkflowTrigger", + "typeVersion": 1 + }, + { + "parameters": { + "conditions": { + "options": { + "version": 2, + "leftValue": "", + "caseSensitive": true, + "typeValidation": "strict" + }, + "conditions": [ + { + "id": "condition-1", + "leftValue": "={{ $json.enrichment_status }}", + "operator": { + "type": "string", + "operation": "notEquals" + }, + "rightValue": "enriched" + } + ], + "combinator": "or" + }, + "options": {} + }, + "id": "check-enrichment-needs", + "name": "Check Enrichment Needs", + "position": [ + 448, + 640 + ], + "type": "n8n-nodes-base.if", + "typeVersion": 2.2 + }, + { + "parameters": { + "assignments": { + "assignments": [ + { + "id": "assign-1", + "name": "success", + "type": "boolean", + "value": true + }, + { + "id": "assign-2", + "name": "series_id", + "type": "string", + "value": "={{ $json.id }}" + }, + { + "id": "assign-3", + "name": "series_name", + "type": "string", + "value": "={{ $json.series_name }}" + }, + { + "id": "assign-4", + "name": "message", + "type": "string", + "value": "Series already fully enriched" + } + ] + }, + "options": {} + }, + "id": "already-enriched", + "name": "Already Enriched", + "position": [ + 848, + 832 + ], + "type": "n8n-nodes-base.set", + "typeVersion": 3.4 + }, + { + "parameters": { + "conditions": { + "options": { + "version": 2, + "leftValue": "", + "caseSensitive": true, + "typeValidation": "strict" + }, + "conditions": [ + { + "id": "condition-1", + "leftValue": "={{ $json.mam_series_id }}", + "operator": { + "type": "string", + "operation": "notEmpty", + "singleValue": true + }, + "rightValue": "" + } + ], + "combinator": "and" + }, + "options": {} + }, + "id": "route-by-success", + "name": "Route by Success", + "position": [ + 1472, + 432 + ], + "type": "n8n-nodes-base.if", + "typeVersion": 2.2 + }, + { + "parameters": { + "promptType": "define", + "text": "=Find the Series ID for: {{ $json.series_name }} by {{ $json.author_folder }}", + "hasOutputParser": true, + "needsFallback": true, + "options": { + "systemMessage": "=You are an expert metadata librarian.\n\n### YOUR GOAL\nFind the unique Series ID (integer) on MyAnonaMouse (MAM) for the input Series Name.\n\n### YOUR TOOL\n- **search_mam**: Input is a search string. It searches Titles, Authors, and Series names. Returns a list of results.\n\n### EXECUTION STEPS\n1. **Direct Search:** Start by searching for the exact Series Name.\n2. **Author Strategy (Fallback):** If the series search fails or returns 0 results:\n - Search for the **Author's Name** (from `author_folder` or `author`).\n - Scan the list of books returned.\n - Look for ANY book that appears to belong to the target series (check `series_info`).\n - If you find a book in the series, extract the Series ID from its `series_info`.\n3. **Analyze:**\n - Look for `series_info` (e.g., `{\"669061\": [\"Series Title\", \"1\"]}`).\n - The Series ID is the key (`669061`).\n4. **Validation:** Ensure the found series name matches the user's request.\n\n### OUTPUT FORMAT\nReturn a single JSON object.\n- `mam_series_id`: The ID found (or null).\n- `found_series_name`: The exact series name from the `series_info` field.", + "maxIterations": 20 + } + }, + "type": "@n8n/n8n-nodes-langchain.agent", + "typeVersion": 3, + "position": [ + 672, + 224 + ], + "id": "882d88ae-7d06-468f-9524-c7dbad701365", + "name": "AI Agent", + "alwaysOutputData": true, + "onError": "continueErrorOutput" + }, + { + "parameters": { + "workflowId": { + "__rl": true, + "value": "c3N3bYrOAy0rNGGq", + "mode": "list", + "cachedResultUrl": "/workflow/c3N3bYrOAy0rNGGq", + "cachedResultName": "MAM Search API Tool" + }, + "workflowInputs": { + "mappingMode": "defineBelow", + "value": { + "search_term": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('search_term', ``, 'string') }}" + }, + "matchingColumns": [ + "search_term" + ], + "schema": [ + { + "id": "search_term", + "displayName": "search_term", + "required": false, + "defaultMatch": false, + "display": true, + "canBeUsedToMatch": true, + "removed": false + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + } + }, + "type": "@n8n/n8n-nodes-langchain.toolWorkflow", + "typeVersion": 2.2, + "position": [ + 752, + 448 + ], + "id": "d8a735da-8f5b-4020-816a-1a889c546d44", + "name": "Call 'MAM Search API Tool'" + }, + { + "parameters": { + "model": { + "__rl": true, + "value": "minimaxai/minimax-m2", + "mode": "list", + "cachedResultName": "minimaxai/minimax-m2" + }, + "responsesApiEnabled": false, + "options": {} + }, + "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi", + "typeVersion": 1.3, + "position": [ + 816, + 656 + ], + "id": "54820ab8-524b-4a1a-a939-6e9d0ecf777f", + "name": "OpenAI Chat Model", + "credentials": { + "openAiApi": { + "id": "QRBx9RMx4KoFwGgl", + "name": "Nvidia account" + } + } + }, + { + "parameters": { + "schemaType": "manual", + "inputSchema": "{\n \"type\": \"object\",\n \"properties\": {\n \"mam_series_id\": {\n \"type\": [\"string\", \"null\"],\n \"description\": \"The unique Series ID found. Null if not found.\"\n },\n \"found_series_name\": {\n \"type\": [\"string\", \"null\"],\n \"description\": \"The name of the series exactly as it appears in the MAM database.\"\n }\n },\n \"required\": [\n \"mam_series_id\",\n \"found_series_name\"\n ]\n}", + "autoFix": true + }, + "type": "@n8n/n8n-nodes-langchain.outputParserStructured", + "typeVersion": 1.3, + "position": [ + 880, + 448 + ], + "id": "662ea129-ecfd-4232-ad37-200ad912ea62", + "name": "Structured Output Parser" + }, + { + "parameters": { + "assignments": { + "assignments": [ + { + "id": "cc0ebae4-c92b-4d03-8786-9f7e4e6438f2", + "name": "mam_series_id", + "value": "={{ $json.output.mam_series_id }}", + "type": "string" + }, + { + "id": "bd249040-fe5c-4776-9ee0-0cececd613d1", + "name": "series_name", + "value": "={{ $json.output.found_series_name }}", + "type": "string" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [ + 1248, + 432 + ], + "id": "890dab5a-2306-40da-b498-564e77d1fb67", + "name": "Edit Fields" + } + ], + "connections": { + "Check Enrichment Needs": { + "main": [ + [ + { + "node": "AI Agent", + "type": "main", + "index": 0 + } + ], + [ + { + "index": 0, + "node": "Already Enriched", + "type": "main" + } + ] + ] + }, + "Execute Workflow Trigger": { + "main": [ + [ + { + "node": "Check Enrichment Needs", + "type": "main", + "index": 0 + } + ] + ] + }, + "Route by Success": { + "main": [ + [], + [] + ] + }, + "Call 'MAM Search API Tool'": { + "ai_tool": [ + [ + { + "node": "AI Agent", + "type": "ai_tool", + "index": 0 + } + ] + ] + }, + "AI Agent": { + "main": [ + [ + { + "node": "Edit Fields", + "type": "main", + "index": 0 + } + ], + [] + ] + }, + "OpenAI Chat Model": { + "ai_languageModel": [ + [ + { + "node": "AI Agent", + "type": "ai_languageModel", + "index": 0 + }, + { + "node": "Structured Output Parser", + "type": "ai_languageModel", + "index": 0 + }, + { + "node": "AI Agent", + "type": "ai_languageModel", + "index": 1 + } + ] + ] + }, + "Structured Output Parser": { + "ai_outputParser": [ + [ + { + "node": "AI Agent", + "type": "ai_outputParser", + "index": 0 + } + ] + ] + }, + "Edit Fields": { + "main": [ + [ + { + "node": "Route by Success", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "settings": { + "saveExecutionProgress": true, + "saveManualExecutions": true, + "saveDataErrorExecution": "all", + "saveDataSuccessExecution": "all", + "executionOrder": "v1" + }, + "triggerCount": 0, + "versionId": "6979f441-1ec7-4907-84ab-0255a9f21c26", + "owner": { + "type": "personal", + "projectId": "FeLO36wNUAcn61Wj", + "projectName": "Ben W ", + "personalEmail": "admin@ben.io" + }, + "parentFolderId": "6tDyZCwqELStb6Ik", + "isArchived": false +} \ No newline at end of file diff --git a/workflows/2tDusEAKIJ6bwDl2.json b/workflows/2tDusEAKIJ6bwDl2.json new file mode 100644 index 0000000..123cb20 --- /dev/null +++ b/workflows/2tDusEAKIJ6bwDl2.json @@ -0,0 +1,829 @@ +{ + "id": "2tDusEAKIJ6bwDl2", + "name": "EVE Character Bazar Monitor", + "nodes": [ + { + "parameters": { + "rule": { + "interval": [ + { + "field": "minutes", + "minutesInterval": 15 + } + ] + } + }, + "id": "Schedule Trigger", + "name": "Schedule Trigger", + "position": [ + 2464, + 16 + ], + "type": "n8n-nodes-base.scheduleTrigger", + "typeVersion": 1.3 + }, + { + "parameters": { + "jsCode": "return $input.all().map(item => {\n return {\n json: {\n ...item.json,\n // Ensure character_id is treated as null if missing/empty\n character_id: item.json.character_id || null\n }\n };\n});" + }, + "id": "Check ID Code", + "name": "Check ID Code", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 2912, + -80 + ], + "notes": "Debug: Check logic" + }, + { + "parameters": { + "url": "={{ $json.character_id ? 'https://evewho.com/api/character/' + $json.character_id : 'http://skip-verification' }}", + "options": {} + }, + "id": "Verify Character", + "name": "Verify Character", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.3, + "position": [ + 3360, + -8 + ], + "continueOnFail": true + }, + { + "parameters": { + "jsCode": "const items = $input.all();\nreturn items.map(item => {\n const eveData = item.json.data; // Response from EveWho\n const compliance = {\n status: 'compliant',\n issues: []\n };\n\n if (!eveData) {\n // If no EveWho data (e.g. no character ID found or API failed), we can't verify mechanics.\n // But we can still let the AI check the text.\n compliance.status = 'manual_review';\n compliance.issues.push('No character ID found or verification failed');\n } else {\n // Check Wallet Balance (must be positive, but API doesn't show wallet)\n // Check Kill Rights (must be 0)\n // EveWho API returns: { info: { kill_rights: ... }, ... }\n // Note: EveWho API structure might vary. Assuming standard ESI-like or EveWho structure.\n \n // Actually, EveWho API is often just scraping or basic info.\n // Let's assume we check what we can.\n \n if (eveData.kill_rights && eveData.kill_rights > 0) {\n compliance.status = 'non_compliant';\n compliance.issues.push('Character has kill rights');\n }\n \n // Security Status check?\n if (eveData.sec_status && eveData.sec_status < -2.0) {\n // compliance.issues.push('Low security status');\n }\n }\n\n item.json.compliance_check = compliance;\n return item;\n});" + }, + "id": "Compliance Logic", + "name": "Compliance Logic", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 3584, + -80 + ] + }, + { + "parameters": { + "jsCode": "const items = $input.all();\nreturn items.map(item => {\n const json = item.json;\n const newItemJson = { ...json }; // Clone to avoid mutation issues\n\n // Ensure post_id is preserved and valid\n if (!newItemJson.post_id) {\n // Try to recover from other fields if possible, or generate debug ID\n newItemJson.post_id = json.id || json.guid || (\"MISSING_\" + Math.random().toString(36).substring(7));\n }\n\n // Map AI output to status/issues\n if (newItemJson.output) {\n let aiData = {};\n if (typeof newItemJson.output === 'string') {\n try {\n aiData = JSON.parse(newItemJson.output);\n } catch (e) {\n aiData = { compliance_status: 'error', compliance_issues: ['Invalid JSON from AI'] };\n }\n } else if (typeof newItemJson.output === 'object') {\n aiData = newItemJson.output;\n }\n \n newItemJson.post_status = aiData.compliance_status || 'pending_manual_review';\n \n const issues = aiData.compliance_issues || [];\n newItemJson.compliance_issues = { issues: Array.isArray(issues) ? issues : [issues] };\n \n if (aiData.character_name) {\n newItemJson.character_name = aiData.character_name;\n }\n } else if (newItemJson.post_status) {\n // Bypass items (already have status)\n if (!newItemJson.compliance_issues) {\n newItemJson.compliance_issues = { issues: [] };\n }\n } else {\n // Fallback\n newItemJson.post_status = 'pending_manual_review';\n newItemJson.compliance_issues = { issues: ['No status determined'] };\n }\n\n return { json: newItemJson, pairedItem: item.pairedItem };\n});" + }, + "id": "Prepare Data", + "name": "Prepare Data", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 4608, + -80 + ] + }, + { + "parameters": { + "promptType": "define", + "text": "=You are a Compliance Officer for the EVE Online Character Bazaar.\nAnalyze the following forum post content.\nRules:\n1. Wallet balance must be positive.\n2. Kill rights must be disclosed (usually 'No kill rights').\n3. Jump clones must be disclosed (location and implants).\n4. Character location must be disclosed.\n\nReturn ONLY a JSON object with this structure:\n{\n \"compliance_status\": \"compliant\" | \"non_compliant\",\n \"compliance_issues\": [\"issue 1\", \"issue 2\"],\n \"character_name\": \"extracted name if found, else null\"\n}\nIf compliant, 'compliance_issues' must be an empty array.\nDo not include markdown formatting (```json). Just the raw JSON string.\n\nPost content is below\n--------------------------\n\n{{ $json.content }}", + "options": {} + }, + "id": "AI Compliance Agent", + "name": "AI Compliance Agent", + "type": "@n8n/n8n-nodes-langchain.agent", + "typeVersion": 1.7, + "position": [ + 4032, + -400 + ] + }, + { + "parameters": { + "model": "z-ai/glm-4.6", + "options": {} + }, + "type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter", + "typeVersion": 1, + "position": [ + 4104, + -176 + ], + "id": "98305d4e-91f6-4ca5-9883-326c4b4941d2", + "name": "OpenRouter Chat Model", + "credentials": { + "openRouterApi": { + "id": "ERgv4rlSLgq8fsK9", + "name": "OpenRouter account" + } + } + }, + { + "parameters": { + "jsCode": "const items = $input.all();\nreturn items.map(item => {\n // This node was previously extracting topic IDs from GUID/Link.\n // We want to keep that behavior for the \"Lookup Posts\" node which uses post_id (topic ID).\n \n const guid = item.json.guid || item.json.link || \"\";\n const match = guid.match(/topic[-/](\\d+)/) || guid.match(/\\/t\\/[^\\/]+\\/(\\d+)/);\n \n if (match) {\n item.json.post_id = match[1];\n }\n \n // Ensure 'ids' field is NOT overwritten here if it's meant for Character IDs later.\n // But wait, 'Prepare ID List' is BEFORE 'Lookup Posts'.\n // And 'Extract Character ID' is BEFORE 'Check ID Code'.\n \n // Let's check the workflow flow again.\n // RSS -> Filter WTS -> Extract Character ID -> Check ID Code -> Verify Character\n // RSS -> Filter WTS -> Prepare ID List -> Lookup Posts\n \n // Ah! There are PARALLEL branches!\n \n return item;\n});" + }, + "id": "Prepare ID List", + "name": "Prepare ID List", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 2016, + -176 + ] + }, + { + "parameters": { + "jsCode": "return $input.all();" + }, + "id": "Filter New/Updated", + "name": "Filter New/Updated", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 2688, + -176 + ] + }, + { + "parameters": { + "conditions": { + "options": { + "version": 2, + "leftValue": "", + "caseSensitive": true, + "typeValidation": "strict" + }, + "conditions": [ + { + "id": "1", + "leftValue": "={{ $json.post_status }}", + "rightValue": "", + "operator": { + "type": "string", + "operation": "isNotEmpty", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "id": "Check Pre-Filter", + "name": "Check Pre-Filter", + "type": "n8n-nodes-base.if", + "typeVersion": 2.2, + "position": [ + 3808, + -80 + ] + }, + { + "parameters": { + "conditions": { + "options": { + "version": 2, + "leftValue": "", + "caseSensitive": true, + "typeValidation": "strict" + }, + "conditions": [ + { + "id": "1", + "leftValue": "={{ $json.extracted_char_id }}", + "rightValue": "", + "operator": { + "type": "string", + "operation": "isNotEmpty", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "id": "Split by ID", + "name": "Split by ID", + "type": "n8n-nodes-base.if", + "typeVersion": 2.2, + "position": [ + 3136, + -80 + ] + }, + { + "parameters": { + "jsCode": "const items = $('Prepare ID List').all();\nconst lookupResults = $input.all();\n\nreturn items.map(item => {\n const postId = item.json.post_id;\n // Ensure we handle potential type mismatches (string vs number)\n const match = lookupResults.find(r => String(r.json.post_id) === String(postId));\n \n item.json.exists = !!match;\n if (match) {\n item.json.db_status = match.json.post_status;\n item.json.db_compliance_issues = match.json.compliance_issues;\n item.json.last_reviewed_at = match.json.last_reviewed_at;\n }\n \n return item;\n});" + }, + "id": "9d0240b8-2a28-46a1-828d-75bcc0e1fe89", + "name": "Merge Manual", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 2464, + -176 + ] + }, + { + "parameters": { + "jsCode": "const aiResults = $input.all(0);\nconst bypassItems = $input.all(1);\nconst originalAiItems = $input.all(2);\n\n// Merge AI results with originals to restore fields\nconst mergedAiItems = originalAiItems.map((item, index) => {\n // Assuming 1-to-1 order with AI results\n const aiResult = aiResults[index]; \n if (aiResult) {\n // Copy AI output fields to original item\n item.json.output = aiResult.json.output;\n item.json.text = aiResult.json.text;\n }\n return item;\n});\n\n// Combine with bypass items\nreturn [...bypassItems, ...mergedAiItems];" + }, + "id": "b9a2804d-fc92-492c-8855-f94f4cb180ed", + "name": "Merge AI Data Code", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 4384, + -176 + ] + }, + { + "parameters": {}, + "id": "f1d2a3f6-7d92-46e5-b192-b1df531581a6", + "name": "Pass Through", + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [ + 4096, + 112 + ] + }, + { + "parameters": { + "jsCode": "const items = $input.all();\nconst newItems = [];\nfor (const item of items) {\n const json = item.json;\n // Strict check for post_id\n if (json.post_id && json.post_id !== \"null\" && String(json.post_id).trim() !== \"\") {\n newItems.push({\n json: {\n id: json.id, // Preserve DB ID for update logic\n post_id: String(json.post_id),\n post_status: json.post_status || 'pending_manual_review',\n compliance_issues: json.compliance_issues || { issues: [] },\n last_reviewed_at: new Date().toISOString()\n }\n });\n }\n}\nreturn newItems;" + }, + "id": "4dd8d766-7b16-4d4f-b84a-07954c90c42d", + "name": "Clean Data Code", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 4832, + -80 + ] + }, + { + "parameters": { + "operation": "executeQuery", + "query": "=SELECT * FROM character_bazaar_posts WHERE post_id IN ('{{ $json.post_id }}')", + "options": {} + }, + "id": "Lookup Posts", + "name": "Lookup Posts", + "type": "n8n-nodes-base.postgres", + "typeVersion": 2.4, + "position": [ + 2240, + -176 + ], + "credentials": { + "postgres": { + "id": "5ENroY8sSuOXtF9l", + "name": "n8n-games" + } + } + }, + { + "parameters": { + "operation": "upsert", + "schema": { + "__rl": true, + "mode": "list", + "value": "public" + }, + "table": { + "__rl": true, + "mode": "list", + "value": "character_bazaar_posts" + }, + "columns": { + "mappingMode": "defineBelow", + "value": { + "character_name": "={{ $json.character_name }}", + "compliance_issues": "={{ $json.compliance_issues }}", + "last_reviewed_at": "={{ $now }}", + "post_id": "={{ $json.post_id }}", + "post_status": "={{ $json.post_status }}" + } + }, + "options": {} + }, + "id": "Store Results", + "name": "Store Results", + "type": "n8n-nodes-base.postgres", + "typeVersion": 2.4, + "position": [ + 5056, + -80 + ], + "credentials": { + "postgres": { + "id": "5ENroY8sSuOXtF9l", + "name": "n8n-games" + } + } + }, + { + "parameters": { + "url": "={{ $json.link }}.rss", + "options": {} + }, + "id": "8417128a-ba4f-48a0-92a7-c5e7e54db8e8", + "name": "Fetch Forum Post", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.1, + "position": [ + 0, + -176 + ] + }, + { + "parameters": { + "jsCode": "const items = $input.all();\n\nreturn items.map(item => {\n const rss = item.json.data;\n \n // DEBUG: Log the actual structure we're working with\n console.log('=== Parse Forum Post DEBUG ===');\n console.log('Raw data type:', typeof rss);\n console.log('Raw data length:', rss ? rss.length : 'null');\n console.log('First 500 chars:', rss ? rss.substring(0, 500) : 'null');\n \n // Extract Creator - try multiple patterns\n let creator = null;\n const creatorPatterns = [\n /<\\/dc:creator>/,\n /(.*?)<\\/dc:creator>/,\n /]*>.*?]* itemprop=\"name\">(.*?)<\\/span>/,\n /itemprop=\"author\"[^>]*>.*?]* itemprop=\"name\">(.*?)<\\/span>/\n ];\n \n for (const pattern of creatorPatterns) {\n const match = rss.match(pattern);\n if (match) {\n creator = match[1];\n console.log('Creator found with pattern:', pattern.toString(), 'Value:', creator);\n break;\n }\n }\n \n // Extract Description - try multiple patterns\n let description = \"\";\n const descPatterns = [\n /<\\/description>/,\n /([\\s\\S]*?)<\\/description>/,\n /
]* itemprop=\"text\">([\\s\\S]*?)<\\/div>/\n ];\n \n for (const pattern of descPatterns) {\n const match = rss.match(pattern);\n if (match) {\n description = match[1];\n console.log('Description found with pattern:', pattern.toString(), 'Length:', description.length);\n break;\n }\n }\n \n // Clean HTML entities\n const cleanDescription = description\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/"/g, '\"')\n .replace(/'/g, \"'\");\n \n // Extract Qsna IDs - search in both raw and cleaned content\n const qsnaRegex = /qsna\\.eu\\/eve\\/characters\\/(\\d+)/g;\n const qsnaMatchesRaw = [...rss.matchAll(qsnaRegex)];\n const qsnaMatchesClean = [...cleanDescription.matchAll(qsnaRegex)];\n \n const qsnaIds = [...new Set([\n ...qsnaMatchesRaw.map(m => m[1]),\n ...qsnaMatchesClean.map(m => m[1])\n ])];\n \n console.log('Qsna matches in raw:', qsnaMatchesRaw.length);\n console.log('Qsna matches in clean:', qsnaMatchesClean.length);\n console.log('Final Qsna IDs:', qsnaIds);\n \n // Extract SkillQ Links - search in both raw and cleaned content\n const skillqRegex = /https?:\\/\\/skillq\\.net\\/char\\/[^\\/\\s]+\\/share\\/[a-f0-9\\-]+/g;\n const skillqMatchesRaw = rss.match(skillqRegex) || [];\n const skillqMatchesClean = cleanDescription.match(skillqRegex) || [];\n \n const skillqLinks = [...new Set([...skillqMatchesRaw, ...skillqMatchesClean])];\n \n console.log('SkillQ matches in raw:', skillqMatchesRaw.length);\n console.log('SkillQ matches in clean:', skillqMatchesClean.length);\n console.log('Final SkillQ links:', skillqLinks);\n \n item.json.creator_name = creator;\n item.json.qsna_ids = qsnaIds;\n item.json.skillq_links = skillqLinks;\n \n console.log('=== END DEBUG ===');\n \n return item;\n});" + }, + "id": "ad7ed8fd-8460-4659-b9e2-ed0c50483b6c", + "name": "Parse Forum Post", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 224, + -176 + ] + }, + { + "parameters": { + "method": "POST", + "url": "https://esi.evetech.net/latest/universe/ids/", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ JSON.stringify([$json.creator_name]) }}", + "options": {} + }, + "id": "b3cd5b60-30d6-4089-b51f-3ccad131d548", + "name": "Resolve Creator ID", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.1, + "position": [ + 448, + -296 + ] + }, + { + "parameters": { + "jsCode": "const items = $input.all();\nconst result = [];\n\n// DEBUG: Log input data structure\nconsole.log('=== Split SkillQ Links DEBUG ===');\nconsole.log('Input items length:', items.length);\nif (items.length > 0) {\n console.log('First item keys:', Object.keys(items[0].json));\n console.log('First item skillq_links:', items[0].json.skillq_links);\n console.log('First item post_id:', items[0].json.post_id);\n console.log('First item temp_id:', items[0].json.temp_id);\n}\n\nfor (const item of items) {\n const links = item.json.skillq_links || [];\n // Add temp_id to group back later\n item.json.temp_id = item.json.post_id || Math.random().toString(36).substring(7);\n\n console.log('Processing item with temp_id:', item.json.temp_id, 'links count:', links.length);\n\n if (links.length === 0) {\n item.json.scrape_skipped = true;\n result.push(item);\n console.log('No links found, marking as scrape_skipped');\n } else {\n console.log('Found links:', links);\n for (const link of links) {\n const newItem = {\n json: {\n ...item.json,\n skillq_url_to_scrape: link,\n scrape_skipped: false\n }\n };\n result.push(newItem);\n console.log('Created item for URL:', link);\n }\n }\n}\n\nconsole.log('Total output items:', result.length);\nconsole.log('=== END Split SkillQ Links DEBUG ===');\nreturn result;" + }, + "id": "21333d91-85ba-4baa-a482-ec59241a076f", + "name": "Split SkillQ Links", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 896, + -176 + ] + }, + { + "parameters": { + "conditions": { + "options": { + "version": 2, + "leftValue": "", + "caseSensitive": true, + "typeValidation": "strict" + }, + "conditions": [ + { + "id": "1", + "leftValue": "={{ $json.scrape_skipped }}.toString()", + "rightValue": "true", + "operator": { + "type": "string", + "operation": "contains" + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "id": "67ec4026-ee76-4818-9018-1a289caefb76", + "name": "If Scrape Needed", + "type": "n8n-nodes-base.if", + "typeVersion": 2.2, + "position": [ + 1120, + -176 + ] + }, + { + "parameters": { + "url": "={{ $json.skillq_url_to_scrape }}", + "options": {} + }, + "id": "36697258-f606-436e-a173-74577c77f502", + "name": "Scrape SkillQ", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.1, + "position": [ + 1344, + -296 + ] + }, + { + "parameters": { + "jsCode": "const items = $input.all();\nconst groups = {};\n\nfor (const item of items) {\n const id = item.json.temp_id;\n if (!groups[id]) {\n groups[id] = { ...item.json, scraped_ids: [], skillq_404: false };\n }\n \n if (!item.json.scrape_skipped) {\n const html = item.json.data || \"\";\n \n // Check for 404\n if (html.includes('')) {\n groups[id].skillq_404 = true;\n } else {\n // Extract ID\n const idMatch = html.match(/images\\.evetech\\.net\\/characters\\/(\\d+)\\/portrait/);\n if (idMatch) {\n groups[id].scraped_ids.push(idMatch[1]);\n }\n }\n }\n}\n\nreturn Object.values(groups).map(json => ({ json }));" + }, + "id": "69371473-c0d1-400a-b7cc-6a3d4f996f87", + "name": "Aggregate SkillQ", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1568, + -176 + ] + }, + { + "parameters": { + "jsCode": "const items = $input.all();\n\nreturn items.map(item => {\n const creatorId = item.json.characters ? item.json.characters[0].id : null;\n const creatorName = item.json.characters ? item.json.characters[0].name : item.json.creator_name;\n const qsnaIds = item.json.qsna_ids || [];\n const scrapedIds = item.json.scraped_ids || [];\n const linkIds = [...new Set([...qsnaIds, ...scrapedIds])]; // Unique IDs\n const title = (item.json.title || \"\").toUpperCase();\n \n let characterId = creatorId;\n let characterName = creatorName;\n let status = \"compliant\"; // Default\n \n // Compliance Logic\n if (item.json.skillq_404) {\n status = \"non_compliant_skillq_404\";\n } else if (linkIds.length > 0) {\n // Check if Creator ID matches any Link ID\n const match = linkIds.some(id => String(id) === String(creatorId));\n if (match) {\n status = \"compliant\";\n characterId = creatorId; // Confirmed\n } else {\n status = \"non_compliant_mismatch\";\n item.json.mismatch_ids = linkIds;\n }\n } else {\n // No Links\n if (title.includes(\"WTS\")) {\n status = \"non_compliant_missing_link\";\n } else if (title.includes(\"WTB\")) {\n status = \"ignored_wtb\";\n }\n }\n \n item.json.character_id = characterId;\n item.json.character_name = characterName;\n item.json.compliance_status_initial = status;\n \n return item;\n});" + }, + "id": "91757929-87e4-480c-b2ca-6addf4e26542", + "name": "Consolidate Data", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1792, + -176 + ] + }, + { + "parameters": { + "jsCode": "const creators = $input.all(0).map(i => i.json);\nconst posts = $input.all(1).map(i => i.json);\n\n// DEBUG: Log input structures\nconsole.log('=== Merge Creator Code DEBUG ===');\nconsole.log('Creators input length:', creators.length);\nconsole.log('Posts input length:', posts.length);\nconsole.log('First creator keys:', creators[0] ? Object.keys(creators[0]) : 'null');\nconsole.log('First post keys:', posts[0] ? Object.keys(posts[0]) : 'null');\nconsole.log('First post skillq_links:', posts[0] ? posts[0].skillq_links : 'null');\nconsole.log('First creator skillq_links:', creators[0] ? creators[0].skillq_links : 'null');\n\nconst result = posts.map((post, index) => {\n const creator = creators[index] || {};\n \n // CRITICAL FIX: Preserve ALL post data first, then add creator data\n // This ensures skillq_links and other post fields are not overwritten\n const merged = {\n json: {\n ...post, // Post data takes priority (includes skillq_links, creator_name, qsna_ids, etc.)\n ...creator // Creator data added (only characters array, should not overwrite existing fields)\n }\n };\n \n // DEBUG: Log merge result\n if (index === 0) {\n console.log('Merged result keys:', Object.keys(merged.json));\n console.log('Merged skillq_links:', merged.json.skillq_links);\n console.log('Merged creator_name:', merged.json.creator_name);\n console.log('Merged qsna_ids:', merged.json.qsna_ids);\n }\n \n return merged;\n});\n\nconsole.log('=== END Merge Creator Code DEBUG ===');\nreturn result;" + }, + "id": "dac9a7f5-bff6-48de-a8a2-8a9222cb0b8a", + "name": "Merge Creator Code", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 672, + -176 + ] + }, + { + "parameters": { + "operation": "executeQuery", + "query": "SELECT * FROM character_bazaar_posts WHERE post_status IN ('new', 'updated') ORDER BY last_reviewed_at ASC NULLS FIRST LIMIT 10;", + "additionalFields": {} + }, + "id": "Poll Pending Posts", + "name": "Poll Pending Posts", + "type": "n8n-nodes-base.postgres", + "typeVersion": 1, + "position": [ + 2688, + 16 + ], + "credentials": { + "postgres": { + "id": "5ENroY8sSuOXtF9l", + "name": "n8n-games" + } + } + } + ], + "connections": { + "Check ID Code": { + "main": [ + [ + { + "node": "Split by ID", + "type": "main", + "index": 0 + } + ] + ] + }, + "Verify Character": { + "main": [ + [ + { + "node": "Compliance Logic", + "type": "main", + "index": 0 + } + ] + ] + }, + "Compliance Logic": { + "main": [ + [ + { + "node": "Check Pre-Filter", + "type": "main", + "index": 0 + } + ] + ] + }, + "OpenRouter Chat Model": { + "ai_languageModel": [ + [ + { + "node": "AI Compliance Agent", + "type": "ai_languageModel", + "index": 0 + } + ] + ] + }, + "Filter New/Updated": { + "main": [ + [ + { + "node": "Check ID Code", + "type": "main", + "index": 0 + } + ] + ] + }, + "Check Pre-Filter": { + "main": [ + [ + { + "node": "Prepare Data", + "type": "main", + "index": 0 + }, + { + "node": "Merge AI Data Code", + "type": "main", + "index": 0 + }, + { + "node": "Pass Through", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "AI Compliance Agent", + "type": "main", + "index": 0 + } + ] + ] + }, + "Split by ID": { + "main": [ + [ + { + "node": "Compliance Logic", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Verify Character", + "type": "main", + "index": 0 + } + ] + ] + }, + "Merge Manual": { + "main": [ + [ + { + "node": "Filter New/Updated", + "type": "main", + "index": 0 + } + ] + ] + }, + "Merge AI Data Code": { + "main": [ + [ + { + "node": "Prepare Data", + "type": "main", + "index": 0 + } + ] + ] + }, + "AI Compliance Agent": { + "main": [ + [ + { + "node": "Merge AI Data Code", + "type": "main", + "index": 0 + } + ] + ] + }, + "Pass Through": { + "main": [ + [ + { + "node": "Merge AI Data Code", + "type": "main", + "index": 0 + } + ] + ] + }, + "Prepare Data": { + "main": [ + [ + { + "node": "Clean Data Code", + "type": "main", + "index": 0 + } + ] + ] + }, + "Prepare ID List": { + "main": [ + [ + { + "node": "Lookup Posts", + "type": "main", + "index": 0 + } + ] + ] + }, + "Lookup Posts": { + "main": [ + [ + { + "node": "Merge Manual", + "type": "main", + "index": 0 + } + ] + ] + }, + "Clean Data Code": { + "main": [ + [ + { + "node": "Store Results", + "type": "main", + "index": 0 + } + ] + ] + }, + "Fetch Forum Post": { + "main": [ + [ + { + "node": "Parse Forum Post", + "type": "main", + "index": 0 + } + ] + ] + }, + "Parse Forum Post": { + "main": [ + [ + { + "node": "Resolve Creator ID", + "type": "main", + "index": 0 + }, + { + "node": "Merge Creator Code", + "type": "main", + "index": 1 + } + ] + ] + }, + "Split SkillQ Links": { + "main": [ + [ + { + "node": "If Scrape Needed", + "type": "main", + "index": 0 + } + ] + ] + }, + "If Scrape Needed": { + "main": [ + [ + { + "node": "Scrape SkillQ", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Aggregate SkillQ", + "type": "main", + "index": 0 + } + ] + ] + }, + "Scrape SkillQ": { + "main": [ + [ + { + "node": "Aggregate SkillQ", + "type": "main", + "index": 0 + } + ] + ] + }, + "Aggregate SkillQ": { + "main": [ + [ + { + "node": "Consolidate Data", + "type": "main", + "index": 0 + } + ] + ] + }, + "Consolidate Data": { + "main": [ + [ + { + "node": "Prepare ID List", + "type": "main", + "index": 0 + } + ] + ] + }, + "Resolve Creator ID": { + "main": [ + [ + { + "node": "Merge Creator Code", + "type": "main", + "index": 0 + } + ] + ] + }, + "Merge Creator Code": { + "main": [ + [ + { + "node": "Split SkillQ Links", + "type": "main", + "index": 0 + } + ] + ] + }, + "Schedule Trigger": { + "main": [ + [ + { + "node": "Poll Pending Posts", + "type": "main", + "index": 0 + } + ] + ] + }, + "Poll Pending Posts": { + "main": [ + [ + { + "node": "Check ID Code", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "settings": { + "executionOrder": "v1", + "saveDataErrorExecution": "all", + "saveDataSuccessExecution": "all", + "saveManualExecutions": true, + "saveExecutionProgress": true, + "callerPolicy": "workflowsFromSameOwner", + "availableInMCP": false + }, + "triggerCount": 0, + "versionId": "87cd1f48-d40b-4a15-81e0-0c4e17058983", + "owner": { + "type": "personal", + "projectId": "FeLO36wNUAcn61Wj", + "projectName": "Ben W ", + "personalEmail": "admin@ben.io" + }, + "parentFolderId": "HWgaFb7kLF649L7l", + "isArchived": false +} \ No newline at end of file diff --git a/workflows/3f-bV5cT-rE_QTQxPEZjk.json b/workflows/3f-bV5cT-rE_QTQxPEZjk.json new file mode 100644 index 0000000..88bed0f --- /dev/null +++ b/workflows/3f-bV5cT-rE_QTQxPEZjk.json @@ -0,0 +1,67 @@ +{ + "id": "3f-bV5cT-rE_QTQxPEZjk", + "name": "change-io-review", + "nodes": [ + { + "parameters": { + "path": "f122c212-7644-4634-8e1f-3a99a8b0e770", + "options": {} + }, + "type": "n8n-nodes-base.webhook", + "typeVersion": 2.1, + "position": [ + -656, + 0 + ], + "id": "998d5e15-941c-4a2b-b155-dec787e105a9", + "name": "Webhook", + "webhookId": "f122c212-7644-4634-8e1f-3a99a8b0e770" + }, + { + "parameters": { + "operation": "text", + "options": {} + }, + "type": "n8n-nodes-base.extractFromFile", + "typeVersion": 1.1, + "position": [ + -448, + 0 + ], + "id": "510007f6-2ef5-490b-a4a1-b35d2b2cbffa", + "name": "Extract from File" + } + ], + "connections": { + "Webhook": { + "main": [ + [ + { + "node": "Extract from File", + "type": "main", + "index": 0 + } + ] + ] + }, + "Extract from File": { + "main": [ + [] + ] + } + }, + "settings": { + "executionOrder": "v1", + "availableInMCP": false + }, + "triggerCount": 1, + "versionId": "3f71c8c4-ce1c-49c8-8822-ba8a05bee6fd", + "owner": { + "type": "personal", + "projectId": "FeLO36wNUAcn61Wj", + "projectName": "Ben W ", + "personalEmail": "admin@ben.io" + }, + "parentFolderId": "of8yoeyjjIAhYdnQ", + "isArchived": false +} \ No newline at end of file diff --git a/workflows/3l7tJfcRoA1T1o6g.json b/workflows/3l7tJfcRoA1T1o6g.json new file mode 100644 index 0000000..0e9bcf1 --- /dev/null +++ b/workflows/3l7tJfcRoA1T1o6g.json @@ -0,0 +1,606 @@ +{ + "id": "3l7tJfcRoA1T1o6g", + "name": "NPM Auth Token Manager", + "nodes": [ + { + "parameters": {}, + "id": "start-node", + "name": "Manual Trigger", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [ + 240, + 320 + ] + }, + { + "parameters": { + "rule": { + "interval": [ + { + "field": "months", + "monthsInterval": 11 + } + ] + } + }, + "id": "schedule-node", + "name": "Schedule Token Refresh", + "type": "n8n-nodes-base.scheduleTrigger", + "typeVersion": 1.2, + "position": [ + 240, + 480 + ], + "notes": "Runs every 11 months to check token validity and refresh if needed" + }, + { + "parameters": { + "jsCode": "// Check if existing token is still valid based on expiration date\nconst items_data = items || [];\n\nconsole.log('=== Token Validation Starting ===');\nconsole.log('Items received:', items_data.length);\n\n// If no token exists or Supabase query returned no data, proceed to generate new one\nif (items_data.length === 0) {\n console.log('❌ No items received from Supabase query.');\n console.log('→ Will generate new token.');\n return [\n {\n json: {\n tokenValid: false,\n reason: 'no_token_found',\n message: 'No token exists in database'\n }\n }\n ];\n}\n\nconst firstItem = items_data[0].json || {};\nconsole.log('First item keys:', Object.keys(firstItem));\n\n// Check if the item has actual token data\nif (Object.keys(firstItem).length === 0 || !firstItem.token) {\n console.log('❌ No token data found in Supabase result.');\n console.log('→ Will generate new token.');\n return [\n {\n json: {\n tokenValid: false,\n reason: 'no_token_found',\n message: 'Token field is empty or missing'\n }\n }\n ];\n}\n\nconst tokenRecord = firstItem;\nconst expiresAt = tokenRecord.expires_at;\n\n// Check if expires_at exists and is in the future\nif (!expiresAt) {\n console.log('⚠️ Token has no expiration date.');\n console.log('→ Will generate new token.');\n return [\n {\n json: {\n tokenValid: false,\n reason: 'no_expiration_date',\n message: 'Token missing expiration date',\n existingToken: tokenRecord\n }\n }\n ];\n}\n\n// Parse expiration date\nconst expirationDate = new Date(expiresAt);\nconst now = new Date();\nconst daysUntilExpiration = Math.floor((expirationDate - now) / (1000 * 60 * 60 * 24));\n\nconsole.log('=== Token Expiration Check ===');\nconsole.log('Current time:', now.toISOString());\nconsole.log('Token expires:', expirationDate.toISOString());\nconsole.log('Days until expiration:', daysUntilExpiration);\n\n// If token expires in less than 30 days, regenerate\nif (daysUntilExpiration < 30) {\n console.log('⚠️ Token expires in less than 30 days.');\n console.log('→ Will generate new token.');\n return [\n {\n json: {\n tokenValid: false,\n reason: 'expiring_soon',\n message: `Token expires in ${daysUntilExpiration} days`,\n daysUntilExpiration: daysUntilExpiration,\n existingToken: tokenRecord\n }\n }\n ];\n}\n\n// Token expiration looks good, proceed to API validation\nconsole.log('✅ Token expiration date is valid (', daysUntilExpiration, 'days remaining)');\nconsole.log('→ Next: Testing token against NPM API...');\nreturn [\n {\n json: {\n tokenValid: true,\n reason: 'expiration_valid',\n message: `Token expires in ${daysUntilExpiration} days`,\n daysUntilExpiration: daysUntilExpiration,\n existingToken: tokenRecord\n }\n }\n];" + }, + "id": "validate-existing-token-node", + "name": "Check Token Expiration", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 640, + 400 + ], + "alwaysOutputData": true, + "notes": "Step 1: Validates token expiration date. Handles all edge cases for empty/missing data. Always returns a clear tokenValid boolean for routing." + }, + { + "parameters": { + "url": "https://npm.dfw.ben.io/api/nginx/proxy-hosts", + "options": { + "timeout": 5000 + }, + "headerParametersUi": { + "parameter": [ + { + "name": "Authorization", + "value": "=Bearer {{ $json.existingToken.token }}" + } + ] + } + }, + "id": "test-token-validity-node", + "name": "Test Token Against NPM API", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 1, + "position": [ + 1040, + 288 + ], + "notes": "Tests if the existing token actually works by making a test API call to NPM. Returns 200 if valid, 401 if invalid." + }, + { + "parameters": { + "jsCode": "// Check if the token actually works with NPM API\nconst response = $input.item.json;\nconst statusCode = response.statusCode || 200;\nconst previousValidation = $('Check Token Expiration').item.json;\n\nconsole.log('=== NPM Token API Test ===');\nconsole.log('API Response Status:', statusCode);\n\n// Token works if we get 200 OK\nif (statusCode === 200 || statusCode === 304) {\n console.log('✅ Token successfully validated against NPM API');\n console.log('Token expires in', previousValidation.daysUntilExpiration, 'days');\n return [{\n json: {\n tokenValid: true,\n apiVerified: true,\n daysUntilExpiration: previousValidation.daysUntilExpiration,\n existingToken: previousValidation.existingToken\n }\n }];\n}\n\n// Token is invalid (401 Unauthorized or other error)\nconsole.log('❌ Token validation failed against NPM API');\nconsole.log('Status code:', statusCode);\nreturn [{\n json: {\n tokenValid: false,\n apiVerified: false,\n reason: 'api_validation_failed',\n statusCode: statusCode,\n daysUntilExpiration: previousValidation.daysUntilExpiration\n }\n}];" + }, + "id": "evaluate-token-test-node", + "name": "Evaluate API Test Result", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1248, + 288 + ], + "notes": "Evaluates the API test response. If 200/304, token is valid. If 401 or other error, token needs regeneration." + }, + { + "parameters": { + "conditions": { + "boolean": [ + { + "value1": "={{ $json.tokenValid }}", + "value2": true + } + ] + }, + "options": {} + }, + "id": "route-based-on-validity-node", + "name": "API Test Passed?", + "type": "n8n-nodes-base.if", + "typeVersion": 2, + "position": [ + 1440, + 400 + ], + "notes": "Routes based on API validation result. True = token valid and working. False = regenerate token." + }, + { + "parameters": { + "jsCode": "console.log('✅ Token fully validated and working!');\nconsole.log('- Expiration: Valid (', $json.daysUntilExpiration, 'days remaining)');\nconsole.log('- API Test: Passed');\nconsole.log('No action needed. Next check in ~11 months.');\n\nreturn [{\n json: {\n status: 'skipped',\n message: 'Existing token is valid and working',\n daysUntilExpiration: $json.daysUntilExpiration,\n expiresAt: $json.existingToken.expires_at,\n apiVerified: true\n }\n}];" + }, + "id": "token-still-valid-node", + "name": "Token Valid - Skip", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1648, + 288 + ], + "notes": "Token passed both expiration and API validation. No regeneration needed." + }, + { + "parameters": { + "method": "POST", + "url": "https://npm.dfw.ben.io/api/tokens", + "authentication": "genericCredentialType", + "genericAuthType": "httpCustomAuth", + "options": {} + }, + "id": "npm-login-node", + "name": "NPM Login", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 1168, + 656 + ], + "credentials": { + "httpCustomAuth": { + "id": "7XEtlKn9X5YzJfiJ", + "name": "NPM Admin Credentials" + } + }, + "notes": "Authenticates with NPM using stored credentials to generate new token" + }, + { + "parameters": { + "url": "https://npm.dfw.ben.io/api/tokens?expiry=1y", + "options": {}, + "headerParametersUi": { + "parameter": [ + { + "name": "Authorization", + "value": "=Bearer {{ $json.token }}" + } + ] + } + }, + "id": "get-long-lived-token-node", + "name": "Get Long-Lived Token", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 1, + "position": [ + 1360, + 656 + ], + "notes": "Requests a 1-year long-lived token" + }, + { + "parameters": { + "jsCode": "// Extract the long-lived token from the response\nconst token = $input.item.json.token;\nconst expiresAt = $input.item.json.expires;\n\nconsole.log('Generated new NPM long-lived token');\nconsole.log('Expires at:', expiresAt);\n\nreturn {\n json: {\n token: token,\n expiresAt: expiresAt,\n generatedAt: new Date().toISOString(),\n tokenPreview: token.substring(0, 20) + '...'\n }\n};" + }, + "id": "extract-token-node", + "name": "Extract Token", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1568, + 656 + ], + "notes": "Extracts and formats the token for storage" + }, + { + "parameters": { + "jsCode": "// Prepare the record for Supabase storage\nconst inputData = $input.item.json;\n\nreturn {\n json: {\n service_name: 'npm_dfw',\n token: inputData.token,\n expires_at: inputData.expiresAt,\n generated_at: inputData.generatedAt,\n last_updated: new Date().toISOString()\n }\n};" + }, + "id": "prepare-token-record", + "name": "Prepare Token Record", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1760, + 656 + ], + "notes": "Formats the token data for Supabase storage" + }, + { + "parameters": { + "jsCode": "// Check if UPDATE found and updated a row\nconst input = $input.item.json || {};\nconst hasData = Object.keys(input).length > 0;\n\nconsole.log('=== Checking UPDATE Result ===');\nconsole.log('Input item:', JSON.stringify(input, null, 2));\nconsole.log('Has data:', hasData);\nconsole.log('Keys:', Object.keys(input));\n\n// Check if we have the key database fields\nconst hasToken = input.token !== undefined && input.token !== null;\nconst hasServiceName = input.service_name !== undefined && input.service_name !== null;\n\nconsole.log('Has token field:', hasToken);\nconsole.log('Has service_name field:', hasServiceName);\n\n// If UPDATE returned token data, it succeeded\nif (hasData && (hasToken || hasServiceName)) {\n console.log('✅ UPDATE succeeded - row was found and updated');\n return { json: { ...input, needsCreate: false, operation: 'updated' } };\n}\n\n// UPDATE found no rows - need to CREATE\nconsole.log('⚠️ UPDATE found no rows or returned empty - will CREATE new row');\nconst tokenData = $('Prepare Token Record').item.json;\nreturn { json: { ...tokenData, needsCreate: true, operation: 'create_needed' } };" + }, + "id": "check-update-result", + "name": "Did Update Succeed?", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 2112, + 656 + ], + "alwaysOutputData": true, + "notes": "Checks if UPDATE succeeded. If no rows were updated, routes to CREATE." + }, + { + "parameters": { + "conditions": { + "boolean": [ + { + "value1": "={{ $json.needsCreate }}", + "value2": true + } + ] + }, + "options": {} + }, + "id": "route-create-check", + "name": "Need to Create?", + "type": "n8n-nodes-base.if", + "typeVersion": 2, + "position": [ + 2288, + 656 + ], + "notes": "Routes to CREATE if UPDATE found no rows (first run). Otherwise skips to success." + }, + { + "parameters": { + "jsCode": "console.log('✅ NPM Token successfully refreshed and stored');\nconsole.log('Token expires at:', $input.item.json.expires_at || 'N/A');\nconsole.log('Operation:', $input.item.json.operation || 'completed');\nconsole.log('Next refresh in ~11 months or when token has <30 days remaining');\n\nreturn {\n json: {\n status: 'success',\n operation: 'token_refreshed',\n message: 'NPM long-lived token refreshed and stored in Supabase',\n expiresAt: $input.item.json.expires_at\n }\n};" + }, + "id": "success-notification", + "name": "Log Success", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 2800, + 848 + ] + }, + { + "parameters": { + "rules": { + "values": [ + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 1 + }, + "conditions": [ + { + "leftValue": "={{ $json.tokenValid }}", + "rightValue": true, + "operator": { + "type": "boolean", + "operation": "equals" + }, + "id": "f484389b-d67b-4d23-a498-4efb7afe257c" + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "Valid - Test API" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 1 + }, + "conditions": [ + { + "leftValue": "={{ $json.tokenValid }}", + "rightValue": false, + "operator": { + "type": "boolean", + "operation": "equals" + }, + "id": "ad992289-38cb-42b1-8eff-ff14b3fa47c4" + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "Invalid - Generate" + } + ] + }, + "options": { + "fallbackOutput": "extra" + } + }, + "id": "switch-token-status", + "name": "Route By Token Status", + "type": "n8n-nodes-base.switch", + "typeVersion": 3, + "position": [ + 848, + 400 + ], + "notes": "Routes based on tokenValid. Case 0: false=Generate. Case 1: true=Test API. Fallback case 2 for unexpected values." + }, + { + "id": "check-existing-token-node", + "name": "Check Existing Token (Postgres)", + "type": "n8n-nodes-base.postgres", + "typeVersion": 1, + "position": [ + 448, + 400 + ], + "parameters": { + "operation": "executeQuery", + "query": "SELECT * FROM npm_tokens WHERE service_name = 'npm_dfw' LIMIT 1;" + }, + "credentials": { + "postgres": { + "id": "Ik8CFyap8ic2Md3M", + "name": "n8n-infra" + } + } + }, + { + "id": "Update Token in Supabase", + "name": "Update Token in Postgres", + "type": "n8n-nodes-base.postgres", + "typeVersion": 1, + "position": [ + 2000, + 400 + ], + "parameters": { + "operation": "executeQuery", + "options": { + "queryParameterValues": "{{$json.token}},{{$json.expires_at}},{{$json.generated_at}},{{$json.last_updated}},{{$json.service_name}}" + }, + "query": "UPDATE npm_tokens SET token = $1, expires_at = $2, generated_at = $3, last_updated = $4 WHERE service_name = $5;" + }, + "credentials": { + "postgres": { + "id": "Ik8CFyap8ic2Md3M", + "name": "n8n-infra" + } + } + }, + { + "id": "Create Token in Supabase", + "name": "Create Token in Postgres", + "type": "n8n-nodes-base.postgres", + "typeVersion": 1, + "position": [ + 2400, + 500 + ], + "parameters": { + "operation": "executeQuery", + "options": { + "queryParameterValues": "{{$json.service_name}},{{$json.token}},{{$json.expires_at}},{{$json.generated_at}},{{$json.last_updated}}" + }, + "query": "INSERT INTO npm_tokens (service_name, token, expires_at, generated_at, last_updated) VALUES ($1, $2, $3, $4, $5);" + }, + "credentials": { + "postgres": { + "id": "Ik8CFyap8ic2Md3M", + "name": "n8n-infra" + } + } + } + ], + "connections": { + "Test Token Against NPM API": { + "main": [ + [ + { + "node": "Evaluate API Test Result", + "type": "main", + "index": 0 + } + ] + ] + }, + "Evaluate API Test Result": { + "main": [ + [ + { + "node": "API Test Passed?", + "type": "main", + "index": 0 + } + ] + ] + }, + "API Test Passed?": { + "main": [ + [ + { + "node": "Token Valid - Skip", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "NPM Login", + "type": "main", + "index": 0 + } + ] + ] + }, + "NPM Login": { + "main": [ + [ + { + "node": "Get Long-Lived Token", + "type": "main", + "index": 0 + } + ] + ] + }, + "Get Long-Lived Token": { + "main": [ + [ + { + "node": "Extract Token", + "type": "main", + "index": 0 + } + ] + ] + }, + "Extract Token": { + "main": [ + [ + { + "node": "Prepare Token Record", + "type": "main", + "index": 0 + } + ] + ] + }, + "Did Update Succeed?": { + "main": [ + [ + { + "node": "Need to Create?", + "type": "main", + "index": 0 + } + ] + ] + }, + "Need to Create?": { + "main": [ + [ + { + "node": "Log Success", + "type": "main", + "index": 0 + }, + { + "node": "Create Token in Postgres", + "type": "main", + "index": 0 + } + ] + ] + }, + "Check Token Expiration": { + "main": [ + [ + { + "node": "Route By Token Status", + "type": "main", + "index": 0 + } + ] + ] + }, + "Route By Token Status": { + "main": [ + [ + { + "node": "Test Token Against NPM API", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "NPM Login", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "NPM Login", + "type": "main", + "index": 0 + } + ] + ] + }, + "Manual Trigger": { + "main": [ + [ + { + "node": "Check Existing Token (Postgres)", + "type": "main", + "index": 0 + } + ] + ] + }, + "Schedule Token Refresh": { + "main": [ + [ + { + "node": "Check Existing Token (Postgres)", + "type": "main", + "index": 0 + } + ] + ] + }, + "Check Existing Token (Postgres)": { + "main": [ + [ + { + "node": "Check Token Expiration", + "type": "main", + "index": 0 + } + ] + ] + }, + "Prepare Token Record": { + "main": [ + [ + { + "node": "Update Token in Postgres", + "type": "main", + "index": 0 + } + ] + ] + }, + "Update Token in Postgres": { + "main": [ + [ + { + "node": "Did Update Succeed?", + "type": "main", + "index": 0 + } + ] + ] + }, + "Create Token in Postgres": { + "main": [ + [ + { + "node": "Log Success", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "settings": { + "executionOrder": "v1", + "callerPolicy": "workflowsFromSameOwner", + "availableInMCP": false + }, + "triggerCount": 1, + "versionId": "a927774e-18ea-41b2-b3da-9dee36c2b7d1", + "owner": { + "type": "personal", + "projectId": "FeLO36wNUAcn61Wj", + "projectName": "Ben W ", + "personalEmail": "admin@ben.io" + }, + "parentFolderId": "of8yoeyjjIAhYdnQ", + "isArchived": false +} \ No newline at end of file diff --git a/workflows/6S41oPplwN1S9Lz0.json b/workflows/6S41oPplwN1S9Lz0.json new file mode 100644 index 0000000..bdce691 --- /dev/null +++ b/workflows/6S41oPplwN1S9Lz0.json @@ -0,0 +1,264 @@ +{ + "id": "6S41oPplwN1S9Lz0", + "name": "MAM Remote File Transfer", + "nodes": [ + { + "parameters": {}, + "id": "trigger-execute-workflow", + "name": "When Called by Another Workflow", + "type": "n8n-nodes-base.executeWorkflowTrigger", + "typeVersion": 1, + "position": [ + 224, + 304 + ] + }, + { + "parameters": { + "jsCode": "// Extract directory path from destination path\nconst item = $input.first();\nconst destPath = item.json.dest_path || '';\n\n// Get everything before the last slash\nconst lastSlash = destPath.lastIndexOf('/');\nconst dirName = lastSlash >= 0 ? destPath.substring(0, lastSlash) : '';\n\nreturn [{\n json: {\n ...item.json,\n dir_name: dirName\n }\n}];" + }, + "id": "code-extract-dirname", + "name": "Extract Directory Name", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 448, + 304 + ] + }, + { + "parameters": { + "authentication": "privateKey", + "command": "=mkdir -p '{{ $('Extract Directory Name').item.json.dir_name }}'" + }, + "id": "ssh-mkdir", + "name": "Create Directory on seed-0", + "type": "n8n-nodes-base.ssh", + "typeVersion": 1, + "position": [ + 672, + 304 + ], + "credentials": { + "sshPrivateKey": { + "id": "vOzhicmbOwx1XDF8", + "name": "seed-0.local.ben.io" + } + } + }, + { + "parameters": { + "authentication": "privateKey", + "command": "=scp -q -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i ~/.ssh/n8n.priv.key '{{ $('Extract Directory Name').item.json.source_path }}' 'root@{{ $('Extract Directory Name').item.json.dest_host }}:\"{{ $('Extract Directory Name').item.json.dest_path }}\"'" + }, + "id": "ssh-scp", + "name": "SCP from seed-1 to seed-0", + "type": "n8n-nodes-base.ssh", + "typeVersion": 1, + "position": [ + 880, + 304 + ], + "credentials": { + "sshPrivateKey": { + "id": "foyggnWz8Sv0OO5w", + "name": "seed-1.dfw.ben.io" + } + } + }, + { + "parameters": { + "authentication": "privateKey", + "command": "=stat -c '%s' '{{ $('Extract Directory Name').item.json.dest_path }}'" + }, + "id": "ssh-verify", + "name": "Verify File on seed-0", + "type": "n8n-nodes-base.ssh", + "typeVersion": 1, + "position": [ + 1104, + 304 + ], + "credentials": { + "sshPrivateKey": { + "id": "vOzhicmbOwx1XDF8", + "name": "seed-0.local.ben.io" + } + } + }, + { + "parameters": { + "jsCode": "// Verify file size matches expected\nconst item = $input.first(); // Output from ssh-verify (stdout)\nconst statOutput = item.json.stdout || item.json.output || '';\nconst actualSize = parseInt(statOutput.trim());\n\n// Get original context from earlier node\nconst originalItem = $('Extract Directory Name').item.json;\nconst expectedSize = originalItem.expected_size;\nconst destPath = originalItem.dest_path;\n\nconst verified = !isNaN(actualSize) && (!expectedSize || actualSize === expectedSize);\n\nreturn [{\n json: {\n ...originalItem, // Preserve full context\n status: verified ? 'success' : 'failed',\n final_path: destPath,\n verified: verified,\n actual_size: actualSize,\n expected_size: expectedSize,\n error: verified ? null : `Size mismatch: expected ${expectedSize}, got ${actualSize}`\n }\n}];" + }, + "id": "code-check-size", + "name": "Check File Size", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1328, + 304 + ] + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict" + }, + "conditions": [ + { + "id": "condition-1", + "leftValue": "={{ $json.verified }}", + "rightValue": true, + "operator": { + "type": "boolean", + "operation": "equals" + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "id": "if-verified", + "name": "File Verified?", + "type": "n8n-nodes-base.if", + "typeVersion": 2, + "position": [ + 1552, + 304 + ] + }, + { + "parameters": { + "respondWith": "allIncomingItems", + "options": {} + }, + "id": "respond-success", + "name": "Respond Success", + "type": "n8n-nodes-base.respondToWebhook", + "typeVersion": 1, + "position": [ + 1984, + 224 + ] + }, + { + "parameters": { + "respondWith": "allIncomingItems", + "options": {} + }, + "id": "respond-failed", + "name": "Respond Failed", + "type": "n8n-nodes-base.respondToWebhook", + "typeVersion": 1, + "position": [ + 1984, + 384 + ] + } + ], + "connections": { + "When Called by Another Workflow": { + "main": [ + [ + { + "node": "Extract Directory Name", + "type": "main", + "index": 0 + } + ] + ] + }, + "Extract Directory Name": { + "main": [ + [ + { + "node": "Create Directory on seed-0", + "type": "main", + "index": 0 + } + ] + ] + }, + "Create Directory on seed-0": { + "main": [ + [ + { + "node": "SCP from seed-1 to seed-0", + "type": "main", + "index": 0 + } + ] + ] + }, + "SCP from seed-1 to seed-0": { + "main": [ + [ + { + "node": "Verify File on seed-0", + "type": "main", + "index": 0 + } + ] + ] + }, + "Verify File on seed-0": { + "main": [ + [ + { + "node": "Check File Size", + "type": "main", + "index": 0 + } + ] + ] + }, + "Check File Size": { + "main": [ + [ + { + "node": "File Verified?", + "type": "main", + "index": 0 + } + ] + ] + }, + "File Verified?": { + "main": [ + [ + { + "node": "Respond Failed", + "type": "main", + "index": 0 + }, + { + "node": "Respond Success", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "settings": { + "saveExecutionProgress": true, + "saveManualExecutions": true, + "saveDataErrorExecution": "all", + "saveDataSuccessExecution": "all", + "executionOrder": "v1" + }, + "triggerCount": 0, + "versionId": "0f762959-8423-4af4-92a9-82e006e0f90c", + "owner": { + "type": "personal", + "projectId": "FeLO36wNUAcn61Wj", + "projectName": "Ben W ", + "personalEmail": "admin@ben.io" + }, + "parentFolderId": "6tDyZCwqELStb6Ik", + "isArchived": false +} \ No newline at end of file diff --git a/workflows/7kAZyLHOpYKg4riN.json b/workflows/7kAZyLHOpYKg4riN.json new file mode 100644 index 0000000..1a9ac15 --- /dev/null +++ b/workflows/7kAZyLHOpYKg4riN.json @@ -0,0 +1,115 @@ +{ + "id": "7kAZyLHOpYKg4riN", + "name": "gemini-cli-vscode", + "nodes": [ + { + "parameters": { + "jsCode": "// Defaults and Input Parsing\nconst input = items[0].json;\n\n// 1. Prompt Handling (Escape quotes)\nconst rawPrompt = input.prompt || \"\";\nconst safePrompt = rawPrompt.replace(/\"/g, '\\\\\"');\n\n// 2. Defaults\nconst model = input.model || \"gemini-3-flash-preview\";\nconst approvalMode = input['approval-mode'] || \"yolo\";\nconst startDir = input['starting-directory'] || \"~/projects/\";\n\n// 3. Construct Command\n// START: Base command with flags FIRST (model, approval-mode)\nconst geminiPath = \"/home/b3nw/.npm-global/bin/gemini\";\nlet cmd = `${geminiPath} --model \"${model}\" --approval-mode \"${approvalMode}\"`;\n\n// Handle Allowed MCP Servers\n// Logic: If input is provided and valid, use it. Otherwise, force \"\" to disable all.\nlet mcpInput = input['allowed-mcp-server-names'];\nlet mcpString = \"\";\n\n// Normalize input to a string (handle array or CSV string)\nif (mcpInput !== undefined && mcpInput !== null) {\n mcpString = Array.isArray(mcpInput) ? mcpInput.join(\" \") : mcpInput.toString().split(\",\").join(\" \");\n}\n\n// Append flag: Use the list if we have one, otherwise explicitly pass \"\"\nif (mcpString && mcpString.trim().length > 0) {\n cmd += ` --allowed-mcp-server-names ${mcpString}`;\n} else {\n // This ensures we explicitly disable MCP tools when no specific servers are named\n cmd += ` --allowed-mcp-server-names \"\"`;\n}\n\n// Handle Include Directories\nlet incDirInput = input['include-directories'];\nif (incDirInput) {\n const incDirList = Array.isArray(incDirInput) ? incDirInput.join(\",\") : incDirInput;\n if (incDirList.trim().length > 0) {\n cmd += ` --include-directories ${incDirList}`;\n }\n}\n\n// Handle Resume\nif (input.resume) {\n cmd += ` --resume \"${input.resume}\"`;\n}\n\n// Force json output\ncmd += ` --output-format json`;\n\n// END: Append the prompt LAST as the positional argument\ncmd += ` \"${safePrompt}\"`;\n\nreturn [\n {\n json: {\n command: cmd,\n starting_directory: startDir\n }\n }\n];" + }, + "id": "61fecf15-8117-4a65-98ee-d3b019d2b89e", + "name": "Construct Command", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 464, + 32 + ] + }, + { + "parameters": { + "authentication": "privateKey", + "command": "={{ $json.command }}", + "cwd": "={{ $json.starting_directory }}" + }, + "id": "4bfebf4e-2390-405e-ae3d-8ce8ff6ddde4", + "name": "Run Gemini via SSH", + "type": "n8n-nodes-base.ssh", + "typeVersion": 1, + "position": [ + 688, + 32 + ], + "credentials": { + "sshPrivateKey": { + "id": "S2dcVMjrpg0I0kdV", + "name": "vscode-dev.local.ben.io" + } + } + }, + { + "parameters": { + "inputSource": "jsonExample", + "jsonExample": "{\n \"prompt\": \"Describe the current directory\",\n \"starting-directory\": \"~/projects/\",\n \"allowed-mcp-server-names\": \"\",\n \"resume\": \"\",\n \"approval-mode\": \"yolo\",\n \"model\": \"gemini-3-flash-preview\",\n \"include-directories\": []\n}" + }, + "type": "n8n-nodes-base.executeWorkflowTrigger", + "typeVersion": 1.1, + "position": [ + 224, + 32 + ], + "id": "6c73c52e-377a-44d3-9265-ade1ec3c0cf7", + "name": "When Executed by Another Workflow" + }, + { + "parameters": { + "jsCode": "// Get raw inputs\nconst inputItem = items[0].json;\nconst rawOutput = inputItem.stdout;\nconst rawError = inputItem.stderr || \"\";\n\ntry {\n // 1. Parse the main JSON output\n const parsed = JSON.parse(rawOutput);\n\n // 2. Add code and signal to the 'stats' object\n // We ensure parsed.stats exists, then merge the new fields in\n parsed.stats = {\n ...parsed.stats,\n exit_code: inputItem.code,\n signal: inputItem.signal\n };\n\n // 3. Clean up Response (Flatten newlines to spaces)\n if (parsed.response && typeof parsed.response === 'string') {\n parsed.response = parsed.response.replace(/\\n+/g, ' ').trim();\n }\n\n // 4. Clean up Logs (Split into an array for better readability)\n // This removes the \\n characters and creates a clean list of strings\n const cleanLogs = rawError.split('\\n').filter(line => line.trim() !== '');\n\n return [{\n json: {\n ...parsed,\n cli_logs: cleanLogs\n }\n }];\n\n} catch (error) {\n // Fallback for parsing errors\n return [{\n json: {\n error: \"Failed to parse CLI output\",\n raw_output: rawOutput,\n // Still try to save the code/signal even on error\n stats: {\n exit_code: inputItem.code,\n signal: inputItem.signal\n },\n cli_logs: rawError.split('\\n')\n }\n }];\n}" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 896, + 32 + ], + "id": "b6372a48-5e24-4dc6-9b9b-692b6eed4ef4", + "name": "Code in JavaScript" + } + ], + "connections": { + "Construct Command": { + "main": [ + [ + { + "node": "Run Gemini via SSH", + "type": "main", + "index": 0 + } + ] + ] + }, + "When Executed by Another Workflow": { + "main": [ + [ + { + "node": "Construct Command", + "type": "main", + "index": 0 + } + ] + ] + }, + "Run Gemini via SSH": { + "main": [ + [ + { + "node": "Code in JavaScript", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "settings": { + "executionOrder": "v1" + }, + "triggerCount": 0, + "versionId": "c77abb27-e648-4382-a9d5-49aa2f38e03d", + "owner": { + "type": "personal", + "projectId": "FeLO36wNUAcn61Wj", + "projectName": "Ben W ", + "personalEmail": "admin@ben.io" + }, + "parentFolderId": "of8yoeyjjIAhYdnQ", + "isArchived": false +} \ No newline at end of file diff --git a/workflows/A2F85aYzVDvQnUrc.json b/workflows/A2F85aYzVDvQnUrc.json new file mode 100644 index 0000000..ef8e043 --- /dev/null +++ b/workflows/A2F85aYzVDvQnUrc.json @@ -0,0 +1,148 @@ +{ + "id": "A2F85aYzVDvQnUrc", + "name": "Project Orchestrator", + "nodes": [ + { + "parameters": { + "jsCode": "// Get raw inputs\nconst inputItem = items[0].json;\nconst rawOutput = inputItem.stdout;\nconst rawError = inputItem.stderr || \"\";\n\n// If stdout is empty but stderr has content, the agent likely failed to start\nif (!rawOutput.trim() && rawError.trim()) {\n throw new Error(`OpenCode Agent failed to produce output. Error logs: ${rawError}`);\n}\n\ntry {\n // OpenCode outputs newline-delimited JSON (NDJSON)\n const lines = rawOutput.split('\\n').filter(line => line.trim() !== '');\n if (lines.length === 0) {\n throw new Error(\"No output events received from agent.\");\n }\n \n const events = lines.map(line => JSON.parse(line));\n\n // Extract text responses (agent's messages to user)\n const textEvents = events.filter(e => e.type === 'text');\n const response = textEvents.map(e => e.part?.text || '').join('\\n').trim();\n\n // Extract tool usage summary\n const toolEvents = events.filter(e => e.type === 'tool_use');\n const tools = toolEvents.map(e => ({\n tool: e.part?.tool,\n status: e.part?.state?.status,\n input: e.part?.state?.input\n }));\n\n // Extract step finish events for token/cost stats\n const stepFinishes = events.filter(e => e.type === 'step_finish');\n \n // Validation: If no steps finished and no text was produced, it's a failure\n if (stepFinishes.length === 0 && textEvents.length === 0) {\n throw new Error(\"Agent session ended prematurely without output.\");\n }\n\n const totalTokens = stepFinishes.reduce((acc, e) => {\n const t = e.part?.tokens || {};\n return {\n input: (acc.input || 0) + (t.input || 0),\n output: (acc.output || 0) + (t.output || 0),\n reasoning: (acc.reasoning || 0) + (t.reasoning || 0)\n };\n }, {});\n const totalCost = stepFinishes.reduce((acc, e) => acc + (e.part?.cost || 0), 0);\n\n // Get session info from first event\n const sessionID = events[0]?.sessionID || null;\n\n // Clean up logs\n const cleanLogs = rawError.split('\\n').filter(line => line.trim() !== '');\n\n return [{\n json: {\n response: response.replace(/\\n+/g, ' ').trim(),\n sessionID,\n stats: {\n exit_code: inputItem.code,\n signal: inputItem.signal,\n total_cost: totalCost,\n tokens: totalTokens,\n steps: stepFinishes.length,\n tool_calls: toolEvents.length\n },\n tools,\n cli_logs: cleanLogs\n }\n }];\n\n} catch (error) {\n // Fail the node if parsing or validation fails\n throw new Error(\"Failed to validate OpenCode output: \" + error.message);\n}\n" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -96, + 48 + ], + "name": "Code in JavaScript", + "id": "2dd84c9e-0fc0-4b34-911e-4a2cea1aae86" + }, + { + "parameters": { + "authentication": "privateKey", + "command": "/home/b3nw/projects/core/project-orchestrator/bin/run-agent.sh", + "cwd": "/home/b3nw/projects/" + }, + "type": "n8n-nodes-base.ssh", + "position": [ + -304, + 48 + ], + "typeVersion": 1, + "id": "f7a8fc32-699d-4e5b-afad-eb269855dbab", + "name": "execute run_agent.sh", + "credentials": { + "sshPrivateKey": { + "id": "S2dcVMjrpg0I0kdV", + "name": "vscode-dev.local.ben.io" + } + } + }, + { + "parameters": { + "authentication": "privateKey", + "command": "/home/b3nw/projects/core/project-orchestrator/bin/session-cleanup.sh", + "cwd": "/home/b3nw/projects/core/project-orchestrator" + }, + "type": "n8n-nodes-base.ssh", + "position": [ + -304, + -128 + ], + "typeVersion": 1, + "id": "67e5e087-a6db-4013-87b3-a7fbb02421d1", + "name": "execute cleanup_sessions.sh", + "credentials": { + "sshPrivateKey": { + "id": "S2dcVMjrpg0I0kdV", + "name": "vscode-dev.local.ben.io" + } + } + }, + { + "parameters": { + "rule": { + "interval": [ + { + "daysInterval": 7, + "triggerAtMinute": 30 + } + ] + } + }, + "type": "n8n-nodes-base.scheduleTrigger", + "typeVersion": 1.3, + "position": [ + -528, + -128 + ], + "id": "fc10c6c7-dd2c-4458-9d75-624f8a22124c", + "name": "Weekly Trigger" + }, + { + "parameters": { + "rule": { + "interval": [ + {} + ] + } + }, + "name": "Nightly Trigger", + "id": "343d37fc-f49d-4a30-969d-11abfed0594f", + "position": [ + -512, + 48 + ], + "type": "n8n-nodes-base.scheduleTrigger", + "typeVersion": 1.3 + } + ], + "connections": { + "execute run_agent.sh": { + "main": [ + [ + { + "type": "main", + "index": 0, + "node": "Code in JavaScript" + } + ] + ] + }, + "Weekly Trigger": { + "main": [ + [ + { + "node": "execute cleanup_sessions.sh", + "type": "main", + "index": 0 + } + ] + ] + }, + "Nightly Trigger": { + "main": [ + [ + { + "node": "execute run_agent.sh", + "index": 0, + "type": "main" + } + ] + ] + } + }, + "settings": { + "executionOrder": "v1", + "callerPolicy": "workflowsFromSameOwner", + "availableInMCP": false + }, + "triggerCount": 2, + "versionId": "ae676149-56f0-4096-89d3-cdb6ab12fe44", + "owner": { + "type": "personal", + "projectId": "FeLO36wNUAcn61Wj", + "projectName": "Ben W ", + "personalEmail": "admin@ben.io" + }, + "parentFolderId": "LTWZD96boqxk9sIs", + "isArchived": false +} \ No newline at end of file diff --git a/workflows/ERCWB3oSYbgsUiqL.json b/workflows/ERCWB3oSYbgsUiqL.json new file mode 100644 index 0000000..6a73f3a --- /dev/null +++ b/workflows/ERCWB3oSYbgsUiqL.json @@ -0,0 +1,270 @@ +{ + "id": "ERCWB3oSYbgsUiqL", + "name": "NPM SSL Certificate Monitor", + "nodes": [ + { + "parameters": { + "rule": { + "interval": [ + { + "field": "cronExpression", + "expression": "0 9 * * *" + } + ] + } + }, + "id": "schedule-trigger", + "name": "Daily SSL Check", + "type": "n8n-nodes-base.scheduleTrigger", + "typeVersion": 1.2, + "position": [ + 240, + 400 + ] + }, + { + "parameters": { + "operation": "getAll", + "tableId": "npm_tokens", + "limit": 1, + "matchType": "allFilters", + "filters": { + "conditions": [ + { + "keyName": "service_name", + "condition": "eq", + "keyValue": "npm_dfw" + } + ] + } + }, + "id": "get-npm-token", + "name": "Get NPM Token", + "type": "n8n-nodes-base.supabase", + "typeVersion": 1, + "position": [ + 448, + 400 + ], + "credentials": { + "supabaseApi": { + "id": "lWyf2ikOGHTTwnSU", + "name": "Supabase account" + } + } + }, + { + "parameters": { + "jsCode": "// Process SSL certificates and check expiration\nconst certificates = $input.all();\nconst today = new Date();\nconst WARNING_DAYS = 7;\n\nconsole.log('=== NPM SSL Certificate Monitor ===');\nconsole.log('Total certificates:', certificates.length);\n\nconst certificateStatus = [];\nlet expiringCerts = [];\nlet skippedCount = 0;\n\nfor (const item of certificates) {\n const cert = item.json;\n \n // Skip if no expiration date or invalid\n if (!cert.expires_on || cert.expires_on === 0) {\n console.log('Skipping certificate (no expiration):', cert.nice_name || cert.id);\n skippedCount++;\n continue;\n }\n \n const domainNames = cert.domain_names?.join(', ') || cert.nice_name || `Certificate ${cert.id}`;\n \n // Create date and validate it\n const expiryDate = new Date(cert.expires_on * 1000);\n if (isNaN(expiryDate.getTime())) {\n console.log('Skipping certificate (invalid date):', domainNames);\n skippedCount++;\n continue;\n }\n \n const daysRemaining = Math.ceil((expiryDate - today) / (1000 * 60 * 60 * 24));\n const isExpiring = daysRemaining <= WARNING_DAYS;\n \n let status = 'OK';\n if (daysRemaining < 0) {\n status = 'EXPIRED';\n } else if (isExpiring) {\n status = 'EXPIRING SOON';\n }\n \n const certInfo = {\n domain: domainNames,\n expiryDate: expiryDate.toISOString().split('T')[0],\n daysRemaining: daysRemaining,\n status: status,\n isExpiring: isExpiring\n };\n \n certificateStatus.push(certInfo);\n \n if (isExpiring) {\n expiringCerts.push(certInfo);\n console.log(`🔴 ${domainNames}: ${daysRemaining} days (expires ${certInfo.expiryDate})`);\n } else {\n console.log(`✅ ${domainNames}: ${daysRemaining} days`);\n }\n}\n\nconsole.log('\\n=== Summary ===');\nconsole.log('Valid certificates:', certificateStatus.length);\nconsole.log('Skipped (invalid/missing expiration):', skippedCount);\nconsole.log('Expiring within', WARNING_DAYS, 'days:', expiringCerts.length);\n\nreturn [{\n json: {\n totalCerts: certificateStatus.length,\n expiringCount: expiringCerts.length,\n hasExpiringCerts: expiringCerts.length > 0,\n allCertificates: certificateStatus,\n expiringCertificates: expiringCerts\n }\n}];" + }, + "id": "process-certificates", + "name": "Process SSL Certificates", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 848, + 400 + ] + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 1 + }, + "conditions": [ + { + "id": "f5646765-356f-4270-b42b-8e86a3a64568", + "leftValue": "={{ $json.hasExpiringCerts }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "id": "check-expiring", + "name": "Any Expiring Certs?", + "type": "n8n-nodes-base.if", + "typeVersion": 2, + "position": [ + 1040, + 400 + ] + }, + { + "parameters": { + "jsCode": "// Format warning email for expiring certificates\nconst data = $input.item.json;\nconst expiringCerts = data.expiringCertificates || [];\n\nlet tableRows = '';\nfor (const cert of expiringCerts) {\n const statusIcon = cert.status === 'EXPIRED' ? '🔴' : '⚠️';\n tableRows += `\n \n ${statusIcon} ${cert.domain}\n ${cert.expiresOnFormatted}\n ${cert.daysRemaining} days\n ${cert.status}\n `;\n}\n\nconst subject = `⚠️ SSL Certificate Expiration Warning - ${expiringCerts.length} Certificate${expiringCerts.length > 1 ? 's' : ''}`;\n\nconst htmlBody = `\n\n\n\n \n\n\n
\n
\n

⚠️ SSL Certificate Expiration Warning

\n
\n \n
\n

Summary

\n
    \n
  • ${data.totalCertificates} SSL certificates checked
  • \n
  • ${expiringCerts.length} certificate${expiringCerts.length > 1 ? 's' : ''} expiring within ${data.warningDays} days
  • \n
  • Checked at: ${new Date(data.checkedAt).toLocaleString('en-US')}
  • \n
\n
\n \n

Certificates Requiring Attention

\n \n \n \n \n \n \n \n \n \n \n ${tableRows}\n \n
DomainExpiration DateDays RemainingStatus
\n \n
\n

Action Required: Please renew or update these SSL certificates in your Nginx Proxy Manager.

\n

Open Nginx Proxy Manager →

\n

This is an automated alert from your NPM SSL Certificate Monitor workflow.

\n
\n
\n\n\n`;\n\nreturn [{\n json: {\n subject: subject,\n htmlBody: htmlBody,\n to: 'admin@ben.io',\n expiringCount: expiringCerts.length\n }\n}];" + }, + "id": "format-warning-email", + "name": "Format Warning Email", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1248, + 320 + ] + }, + { + "parameters": {}, + "id": "manual-trigger", + "name": "Manual Trigger", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [ + 240, + 272 + ] + }, + { + "parameters": { + "sendTo": "={{ $json.to }}", + "subject": "={{ $json.subject }}", + "message": "={{ $json.htmlBody }}", + "options": {} + }, + "id": "send-gmail", + "name": "Send Gmail Alert", + "type": "n8n-nodes-base.gmail", + "typeVersion": 2.1, + "position": [ + 1440, + 400 + ], + "webhookId": "c9d0a896-4275-4387-a2c8-af1f138559a9", + "credentials": { + "gmailOAuth2": { + "id": "Os1ux3h3zFlC2XkG", + "name": "Gmail account" + } + } + }, + { + "parameters": { + "url": "https://npm.dfw.ben.io/api/nginx/certificates", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "Authorization", + "value": "=Bearer {{ $json.token }}" + } + ] + }, + "options": {} + }, + "id": "fetch-certs", + "name": "Fetch Certificates", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 640, + 400 + ] + } + ], + "connections": { + "Daily SSL Check": { + "main": [ + [ + { + "node": "Get NPM Token", + "type": "main", + "index": 0 + } + ] + ] + }, + "Get NPM Token": { + "main": [ + [ + { + "node": "Fetch Certificates", + "type": "main", + "index": 0 + } + ] + ] + }, + "Process SSL Certificates": { + "main": [ + [ + { + "node": "Any Expiring Certs?", + "type": "main", + "index": 0 + } + ] + ] + }, + "Manual Trigger": { + "main": [ + [ + { + "node": "Get NPM Token", + "type": "main", + "index": 0 + } + ] + ] + }, + "Format Warning Email": { + "main": [ + [ + { + "node": "Send Gmail Alert", + "type": "main", + "index": 0 + } + ] + ] + }, + "Any Expiring Certs?": { + "main": [ + [ + { + "node": "Format Warning Email", + "type": "main", + "index": 0 + } + ] + ] + }, + "Fetch Certificates": { + "main": [ + [ + { + "node": "Process SSL Certificates", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "settings": { + "saveExecutionProgress": true, + "saveManualExecutions": true, + "saveDataErrorExecution": "all", + "saveDataSuccessExecution": "all", + "executionOrder": "v1" + }, + "triggerCount": 1, + "versionId": "708d8124-5275-445c-92da-43b0f43e9826", + "owner": { + "type": "personal", + "projectId": "FeLO36wNUAcn61Wj", + "projectName": "Ben W ", + "personalEmail": "admin@ben.io" + }, + "parentFolderId": "eWW72giJDI4fxlWw", + "isArchived": false +} \ No newline at end of file diff --git a/workflows/EVctKxhQ2eyGd3gD.json b/workflows/EVctKxhQ2eyGd3gD.json new file mode 100644 index 0000000..3f9bd55 --- /dev/null +++ b/workflows/EVctKxhQ2eyGd3gD.json @@ -0,0 +1,196 @@ +{ + "id": "EVctKxhQ2eyGd3gD", + "name": "GTasks: Get All Tasks", + "nodes": [ + { + "parameters": { + "inputSource": "passthrough" + }, + "type": "n8n-nodes-base.executeWorkflowTrigger", + "typeVersion": 1.1, + "position": [ + -496, + -32 + ], + "id": "4d0aa270-fd61-4aaf-94e0-38546c9f332a", + "name": "When Executed by Another Workflow" + }, + { + "parameters": { + "url": "https://tasks.googleapis.com/tasks/v1/users/@me/lists", + "authentication": "predefinedCredentialType", + "nodeCredentialType": "googleTasksOAuth2Api", + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.3, + "position": [ + -288, + -32 + ], + "id": "27dac4dc-08a7-4dd5-b526-a600d7aa3133", + "name": "HTTP Request", + "credentials": { + "googleTasksOAuth2Api": { + "id": "ErcYIjq8Xs0GRaPF", + "name": "ben.io-gtasks" + } + } + }, + { + "parameters": { + "fieldToSplitOut": "items", + "options": {} + }, + "type": "n8n-nodes-base.splitOut", + "typeVersion": 1, + "position": [ + -80, + -32 + ], + "id": "d46ab354-a184-4dfc-b378-ac31cd986be4", + "name": "Split Out" + }, + { + "parameters": { + "url": "=https://tasks.googleapis.com/tasks/v1/lists/{{ $json.id }}/tasks", + "authentication": "predefinedCredentialType", + "nodeCredentialType": "googleTasksOAuth2Api", + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.3, + "position": [ + 128, + -32 + ], + "id": "d7de2397-4e7f-4b27-aeed-231959bf4939", + "name": "Get List of Tasks", + "credentials": { + "googleTasksOAuth2Api": { + "id": "ErcYIjq8Xs0GRaPF", + "name": "ben.io-gtasks" + } + } + }, + { + "parameters": { + "mode": "raw", + "jsonOutput": "={{ \n (() => {\n // We don't need to find listId outside the loop anymore\n \n const cleanTasks = $json.items.map(t => {\n // FIX: Extract List ID from the 'selfLink' URL\n // regex captures the text between \"lists/\" and \"/tasks\"\n const listId = t.selfLink.match(/lists\\/([^\\/]+)\\/tasks/)[1];\n\n return {\n \"list_id\": listId, \n \"task_id\": t.id,\n \"title\": t.title,\n \"status\": t.status,\n \"due\": t.due,\n \"updated\": t.updated,\n \"notes\": t.notes,\n \"links\": t.links,\n \"webViewLink\": t.webViewLink\n };\n });\n\n return { \"tasks\": cleanTasks };\n })() \n}}", + "options": {} + }, + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [ + 528, + -32 + ], + "id": "9aa7eef0-f197-4491-bbff-39f52343e050", + "name": "Edit Fields" + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "564f2ac0-81d2-4c47-a090-7a7e73dff62b", + "leftValue": "={{ $json.items }}", + "rightValue": 1, + "operator": { + "type": "array", + "operation": "lengthGt", + "rightType": "number" + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "type": "n8n-nodes-base.filter", + "typeVersion": 2.3, + "position": [ + 336, + -32 + ], + "id": "6ff11d12-d94a-4148-bb21-cda69e06536d", + "name": "Filter Empty List" + } + ], + "connections": { + "When Executed by Another Workflow": { + "main": [ + [ + { + "node": "HTTP Request", + "type": "main", + "index": 0 + } + ] + ] + }, + "HTTP Request": { + "main": [ + [ + { + "node": "Split Out", + "type": "main", + "index": 0 + } + ] + ] + }, + "Split Out": { + "main": [ + [ + { + "node": "Get List of Tasks", + "type": "main", + "index": 0 + } + ] + ] + }, + "Get List of Tasks": { + "main": [ + [ + { + "node": "Filter Empty List", + "type": "main", + "index": 0 + } + ] + ] + }, + "Filter Empty List": { + "main": [ + [ + { + "node": "Edit Fields", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "settings": { + "executionOrder": "v1" + }, + "triggerCount": 0, + "versionId": "993969fa-c72b-43e0-8a7e-498badc87e2c", + "owner": { + "type": "personal", + "projectId": "FeLO36wNUAcn61Wj", + "projectName": "Ben W ", + "personalEmail": "admin@ben.io" + }, + "parentFolderId": "OJ2UfPNUOAOHlllh", + "isArchived": false +} \ No newline at end of file diff --git a/workflows/H6TZCHyiYOr1X6Xf.json b/workflows/H6TZCHyiYOr1X6Xf.json new file mode 100644 index 0000000..983328a --- /dev/null +++ b/workflows/H6TZCHyiYOr1X6Xf.json @@ -0,0 +1,1181 @@ +{ + "id": "H6TZCHyiYOr1X6Xf", + "name": "MAM Anime Series List", + "nodes": [ + { + "parameters": {}, + "id": "b3487fff-82e0-4b4d-bc57-01163e435ea2", + "name": "Workflow Trigger", + "position": [ + 48, + 304 + ], + "type": "n8n-nodes-base.executeWorkflowTrigger", + "typeVersion": 1 + }, + { + "parameters": { + "authentication": "privateKey", + "command": "find /mnt/nas/Anime/Audiobooks -mindepth 2 -maxdepth 2 -type d" + }, + "id": "ddae105c-cbb9-4747-8176-9df55a98cce5", + "name": "List General Folders", + "position": [ + 272, + 208 + ], + "type": "n8n-nodes-base.ssh", + "typeVersion": 1, + "credentials": { + "sshPrivateKey": { + "id": "vOzhicmbOwx1XDF8", + "name": "seed-0.local.ben.io" + } + } + }, + { + "parameters": { + "jsCode": "const output = $input.item.json.stdout || \"\";\nconst lines = output.split(String.fromCharCode(10)).filter(l => l.trim());\nreturn lines.map(l => ({\n json: {\n smb_path: l.trim(),\n folder_name: l.split('/').pop()\n }\n}));" + }, + "id": "4a7486cd-5101-46fc-b1de-698b6c675fc8", + "name": "Code", + "position": [ + 624, + 128 + ], + "type": "n8n-nodes-base.code", + "typeVersion": 2 + }, + { + "parameters": { + "promptType": "define", + "text": "=### ROLE\nYou are an expert Data Extraction Specialist for an audiobook library (focus: Light Novels/Anime). You are currently \"blind\" to the book's details and MUST use the provided tools to discover the metadata. You cannot generate the final JSON without data from the tools.\n\n### INPUT DATA\n- **Folder Name:** {{ $json.folder_name }}\n- **SMB Path:** {{ $json.smb_path }}\n\n### STRICT PROCESS\n1. **Analyze Input:**\n - **ASIN Check:** Does 'Folder Name' start with an ASIN (e.g., 'B0...')?\n - **Context Extraction:** Parse the `smb_path`. The folder *above* the current one often holds the **Series Name**. Use this to anchor your search queries.\n\n2. **Execute Tools (Tiered Strategy):**\n *Goal: Identify Title, Author, and ASIN using the most quota-efficient tools first.*\n\n **TIER 1: Direct Lookup (High Confidence)**\n - IF ASIN found in folder: Call `audible_lookup_asin` immediately.\n\n **TIER 2: Primary Search (Google / Brave)**\n - IF NO ASIN: Clean the folder name. Remove text inside brackets `[...]` (e.g., [Yen Audio], [PZG]) and remove \"(Audiobook)\".\n - Construct a query: `\" Light Novel Audiobook\"`.\n - **Action:** Call `Google Search`, `brave_search`.\n - **Verification:** If the search finds an Audible link or a clear Goodreads/Wikipedia entry, use `audible_search_book` to confirm the ASIN, or `audible_lookup_asin` on a ASIN to confirm match to book you are searching for.\n - **Fallback:** If the first tool yields poor results (no clear match), try the *other* Tier 1 tool (e.g., if Google failed, try Brave).\n\n **TIER 3: Deep Search (Tavily - Low Priority)**\n - **Condition:** Only proceed here if Tier 1 and 2 failed or produced conflicting data.\n - **Action:** Call `tavily_search` with a detailed query.\n - **Extraction:** If an `audible.com/series/` URL was found in previous steps, use `tavily_extract` on that URL to scrape the full book list for accurate volume mapping.\n\n3. **Verify:**\n - Does the tool result match the Author/Series found in the `smb_path` context?\n - Ensure you haven't confused the Manga adaptation for the Light Novel.\n\n4. **Finalize:** Construct the JSON output using strictly the data returned by the tools.\n\n### OUTPUT RULES\n- Your final response must be valid JSON.\n- If all items in template are not filled out, set \"identified\" to `false`.\n\n### JSON TEMPLATE (Fill with TOOL DATA only)\n{\n \"title\": \"\",\n \"series\": \"\",\n \"volume_num\": \"\",\n \"asin\": \"\",\n \"author\": \"\",\n \"smb_path\": \"{{ $json.smb_path }}\",\n \"folder_name\": \"{{ $json.folder_name }}\",\n \"identified\": true,\n \"failure_reason\": null\n}", + "hasOutputParser": true, + "options": { + "maxIterations": 10, + "returnIntermediateSteps": false, + "enableStreaming": false + } + }, + "type": "@n8n/n8n-nodes-langchain.agent", + "typeVersion": 3, + "position": [ + 1904, + 240 + ], + "id": "9f9c7b50-2fd4-48ac-9080-a74158f58285", + "name": "Lookup Agent", + "onError": "continueErrorOutput" + }, + { + "parameters": { + "query": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('Query', ``, 'string') }}", + "options": { + "topic": "general", + "search_depth": "basic", + "max_results": 3, + "include_domains": [ + "audible.com", + "amazon.com" + ] + } + }, + "type": "@tavily/n8n-nodes-tavily.tavilyTool", + "typeVersion": 1, + "position": [ + 1952, + 608 + ], + "id": "e2f55ae4-2858-4b74-a355-f33b127396fb", + "name": "tavily_search", + "credentials": { + "tavilyApi": { + "id": "y1IVnGq2Wiqy3HBf", + "name": "Tavily account" + } + } + }, + { + "parameters": { + "operation": "upsert", + "schema": { + "__rl": true, + "mode": "list", + "value": "public" + }, + "table": { + "__rl": true, + "value": "smb_general_books", + "mode": "list", + "cachedResultName": "smb_general_books" + }, + "columns": { + "mappingMode": "defineBelow", + "value": { + "identified": "={{ $('Merge').item.json.output.identified }}", + "needs_review": false, + "volume_num": "={{ $('Merge').item.json.output.volume_num }}", + "smb_path": "={{ $('Merge').item.json.output.smb_path }}", + "book_folder": "={{ $('Merge').item.json.output.folder_name }}", + "author_folder": "={{ $('Merge').item.json.output.author }}", + "book_name": "={{ $('Merge').item.json.output.title }}", + "series_name": "={{ $('Merge').item.json.output.series }}", + "audible_asin": "={{ $('Merge').item.json.output.asin }}", + "failure_reason": "={{ $('Merge').item.json.output.failure_reason }}", + "category": "anime", + "created_at": "={{ $now }}", + "updated_at": "={{ $now }}" + }, + "matchingColumns": [ + "smb_path" + ], + "schema": [ + { + "id": "id", + "displayName": "id", + "required": false, + "defaultMatch": true, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "smb_path", + "displayName": "smb_path", + "required": true, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "book_folder", + "displayName": "book_folder", + "required": true, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": false + }, + { + "id": "author_folder", + "displayName": "author_folder", + "required": true, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": false + }, + { + "id": "book_name", + "displayName": "book_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": false + }, + { + "id": "series_name", + "displayName": "series_name", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": false + }, + { + "id": "audible_asin", + "displayName": "audible_asin", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": false + }, + { + "id": "identified", + "displayName": "identified", + "required": false, + "defaultMatch": false, + "display": true, + "type": "boolean", + "canBeUsedToMatch": false + }, + { + "id": "needs_review", + "displayName": "needs_review", + "required": false, + "defaultMatch": false, + "display": true, + "type": "boolean", + "canBeUsedToMatch": false + }, + { + "id": "retry_stage", + "displayName": "retry_stage", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": false + }, + { + "id": "failure_reason", + "displayName": "failure_reason", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": false + }, + { + "id": "category", + "displayName": "category", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": false + }, + { + "id": "volume_num", + "displayName": "volume_num", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": false + }, + { + "id": "created_at", + "displayName": "created_at", + "required": false, + "defaultMatch": false, + "display": true, + "type": "dateTime", + "canBeUsedToMatch": false + }, + { + "id": "updated_at", + "displayName": "updated_at", + "required": false, + "defaultMatch": false, + "display": true, + "type": "dateTime", + "canBeUsedToMatch": false + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {} + }, + "type": "n8n-nodes-base.postgres", + "typeVersion": 2.6, + "position": [ + 3184, + 224 + ], + "id": "a9cf32b5-56aa-4754-a36a-f6f0f37f6d16", + "name": "Insert or update rows in a table", + "credentials": { + "postgres": { + "id": "9grzZwW7Br6SzdV8", + "name": "n8n-media" + } + }, + "onError": "continueErrorOutput" + }, + { + "parameters": { + "options": { + "reset": false + } + }, + "type": "n8n-nodes-base.splitInBatches", + "typeVersion": 3, + "position": [ + 1536, + 208 + ], + "id": "32dc1b6a-c722-412e-b6a3-ce9282501b37", + "name": "Loop Over Items" + }, + { + "parameters": { + "toolDescription": "google_search accepts title and author and returns the first page of google search results.", + "method": "POST", + "url": "https://google.serper.dev/search", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "X-API-KEY", + "value": "dea8a4e00208f156de1253b4dc4d32f56a1d72bd" + } + ] + }, + "sendBody": true, + "bodyParameters": { + "parameters": [ + { + "name": "q", + "value": "=site:audible.com {{ $fromAI('query') }}" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.httpRequestTool", + "typeVersion": 4.3, + "position": [ + 2064, + 752 + ], + "id": "92174474-1946-4ede-a8fa-4df134d46cbf", + "name": "google_search" + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "b89944ac-44d6-47a6-8824-bc990db96084", + "leftValue": "={{ $json.smb_path }}", + "rightValue": "Pack", + "operator": { + "type": "string", + "operation": "notContains" + } + }, + { + "id": "4801810b-0ceb-4c17-893f-4591279754a8", + "leftValue": "={{ $json.folder_name }}", + "rightValue": ".@__thumb", + "operator": { + "type": "string", + "operation": "notEquals" + } + }, + { + "id": "fccbabbf-14ab-42fc-9b9c-4941c229c764", + "leftValue": "={{ $json.smb_path }}", + "rightValue": "Progressive", + "operator": { + "type": "string", + "operation": "notContains" + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "type": "n8n-nodes-base.filter", + "typeVersion": 2.3, + "position": [ + 1152, + 208 + ], + "id": "04d076e5-c037-431d-b573-e0bfdabaab7d", + "name": "Filter Unwanted", + "notesInFlow": true, + "notes": "- Filter non-book folders\n- Filter Aptiv workout audio" + }, + { + "parameters": { + "mode": "combine", + "combineBy": "combineByPosition", + "options": {} + }, + "type": "n8n-nodes-base.merge", + "typeVersion": 3.2, + "position": [ + 2448, + 240 + ], + "id": "e91c22e9-b231-4334-8318-c5fa03f89fb5", + "name": "Merge" + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "0b89f507-1f5c-446b-912c-f94d94320483", + "leftValue": "={{ $json.status }}", + "rightValue": "FAIL", + "operator": { + "type": "string", + "operation": "notEquals" + } + }, + { + "id": "d308a2e3-28d8-4922-bfc0-4836a5adc612", + "leftValue": "={{ $('Merge').item.json.output.identified }}", + "rightValue": true, + "operator": { + "type": "boolean", + "operation": "equals" + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "type": "n8n-nodes-base.if", + "typeVersion": 2.3, + "position": [ + 2928, + 240 + ], + "id": "540cbeb8-2020-4c64-be9c-07b13ba241b2", + "name": "verified" + }, + { + "parameters": {}, + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [ + 3472, + 736 + ], + "id": "c407ab66-7d47-4246-b9b1-2cc22e9e8aec", + "name": "Formatting" + }, + { + "parameters": { + "schemaType": "manual", + "inputSchema": "{\n \"type\": \"object\",\n \"properties\": {\n \"title\": { \"type\": \"string\" },\n \"series\": { \"type\": [\"string\", \"null\"] },\n \"volume_num\": { \n \"type\": [\"string\", \"number\", \"null\"],\n \"description\": \"The volume number (can be string '3' or number 3)\" \n },\n \"asin\": { \"type\": [\"string\", \"null\"] },\n \"author\": { \"type\": \"string\" },\n \"smb_path\": { \"type\": \"string\" },\n \"folder_name\": { \"type\": \"string\" },\n \"identified\": { \"type\": \"boolean\" },\n \"failure_reason\": { \n \"type\": [\"string\", \"null\"],\n \"description\": \"Reason for failure, or null if successful\" \n }\n },\n \"required\": [\n \"title\",\n \"series\",\n \"volume_num\",\n \"asin\",\n \"author\",\n \"smb_path\",\n \"folder_name\",\n \"identified\",\n \"failure_reason\"\n ]\n}", + "autoFix": true + }, + "type": "@n8n/n8n-nodes-langchain.outputParserStructured", + "typeVersion": 1.3, + "position": [ + 2128, + 448 + ], + "id": "190844e8-ad9c-4406-8c29-d9d6f543869e", + "name": "Structured Output Parser" + }, + { + "parameters": { + "mode": "combine", + "fieldsToMatchString": "smb_path", + "joinMode": "keepNonMatches", + "outputDataFrom": "input1", + "options": {} + }, + "id": "f053ed65-f54a-4da2-8f5f-d2d10b8a4dcd", + "name": "Filter Existing", + "position": [ + 944, + 208 + ], + "type": "n8n-nodes-base.merge", + "typeVersion": 3.2 + }, + { + "parameters": { + "model": { + "__rl": true, + "value": "openai/gpt-oss-120b", + "mode": "list", + "cachedResultName": "openai/gpt-oss-120b" + }, + "responsesApiEnabled": false, + "options": { + "responseFormat": "json_object", + "temperature": 0 + } + }, + "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi", + "typeVersion": 1.3, + "position": [ + 2208, + 640 + ], + "id": "884680ff-1bc3-4ff5-bb29-47eca4083f4c", + "name": "Ollama - gpt-oss:20b - Validator", + "credentials": { + "openAiApi": { + "id": "QRBx9RMx4KoFwGgl", + "name": "Nvidia account" + } + } + }, + { + "parameters": { + "rule": { + "interval": [ + {} + ] + } + }, + "type": "n8n-nodes-base.scheduleTrigger", + "typeVersion": 1.3, + "position": [ + 48, + 144 + ], + "id": "240e422b-cb1b-4e78-895c-1e4892bf0036", + "name": "Schedule Trigger" + }, + { + "parameters": { + "operation": "select", + "schema": { + "__rl": true, + "mode": "list", + "value": "public" + }, + "table": { + "__rl": true, + "value": "smb_general_books", + "mode": "list", + "cachedResultName": "smb_general_books" + }, + "returnAll": true, + "where": { + "values": [ + { + "column": "category", + "value": "anime" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.postgres", + "typeVersion": 2.6, + "position": [ + 592, + 320 + ], + "id": "8b322485-f5bc-4ef5-ac3e-74e8438b7ed1", + "name": "Select rows from a table", + "executeOnce": true, + "credentials": { + "postgres": { + "id": "9grzZwW7Br6SzdV8", + "name": "n8n-media" + } + } + }, + { + "parameters": { + "description": "Call this tool to retrieve a list of the top brave search results.", + "workflowId": { + "__rl": true, + "value": "VUwFjFF2UhNout2T", + "mode": "list", + "cachedResultUrl": "/workflow/VUwFjFF2UhNout2T", + "cachedResultName": "tool_brave_search" + }, + "workflowInputs": { + "mappingMode": "defineBelow", + "value": { + "query": "=site:audible.com {{ $fromAI('query') }}" + }, + "matchingColumns": [ + "query" + ], + "schema": [ + { + "id": "query", + "displayName": "query", + "required": false, + "defaultMatch": false, + "display": true, + "canBeUsedToMatch": true, + "type": "string", + "removed": false + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + } + }, + "type": "@n8n/n8n-nodes-langchain.toolWorkflow", + "typeVersion": 2.2, + "position": [ + 2352, + 672 + ], + "id": "e485683e-6882-4ff5-b739-bd703d109693", + "name": "brave_search" + }, + { + "parameters": { + "model": { + "__rl": true, + "value": "minimax/minimax-m2.1", + "mode": "list", + "cachedResultName": "minimax/minimax-m2.1" + }, + "responsesApiEnabled": false, + "options": { + "temperature": 0.1 + } + }, + "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi", + "typeVersion": 1.3, + "position": [ + 1760, + 448 + ], + "id": "c12548ff-b760-416d-8f63-f8eb57bb66d8", + "name": "OpenAI Chat Model", + "credentials": { + "openAiApi": { + "id": "9UFJNgYndzFIHJAf", + "name": "Nano-GPT" + } + } + }, + { + "parameters": { + "promptType": "define", + "text": "=### ROLE\nYou are a Quality Assurance Auditor.\n**Golden Rule:** The folder missing data is never a failure reason.\n\n### INPUTS\n1. **Source Folder:** {{ $json.folder_name }}\n2. **Source Path:** {{ $json.smb_path }}\n3. **Candidate Metadata:**\n{{ JSON.stringify($json.output) }}\n\n### VALIDATION RULES (Pass/Fail Criteria)\nYou must evaluate the Candidate Metadata against the Source Folder on these specific points. If ANY point fails, the entire validation fails.\n\n#### 1. Title & Series Check (Hierarchy Rule)\n - **Series Match:** If the Folder Name significantly matches the **Candidate Series** (e.g. Folder \"Vampire Hunter D\", Candidate Series \"Vampire Hunter D\"), then the **Candidate Title** is ACCEPTED as valid enrichment.\n - **Title Match:** If the Folder Name matches the **Candidate Title**, that is also a PASS.\n - **FAIL Condition:** Fail ONLY if the Folder Name matches *neither* the Candidate Title *nor* the Candidate Series.\n - *Example PASS:* Folder \"Vampire Hunter D Vol 8\", Candidate Series \"Vampire Hunter D\", Candidate Title \"Mysterious Journey\". (Matches Series).\n\n\n3. **Volume/Series Logic (Conflict Check ONLY):**\n - **Enrichment is Allowed:** If the folder does NOT mention a series or volume, but the Candidate DOES, this is ACCEPTABLE. Do not fail.\n - **Volume 1** may not have a volume number in the title, this is always acceptable.\n - **Contradiction Check:** Only fail if the folder *explicitly* lists a number that conflicts with the Candidate.\n - *Example FAIL:* Folder says \"Book 4\", Candidate says \"volume_num: 8\".\n - *Example PASS:* Folder says \"Star Wars\", Candidate says \"Series: Star Wars, Vol: 6\". (Enrichment).\n\n4. **ASIN Presence:**\n - If the folder contains an ASIN (starts with 'B0'), does the candidate ASIN match it exactly?\n - The Candidate Metadata ASIN MUST be present to pass.\n\n### OUTPUT FORMAT\nOutput a single JSON object. Do not output markdown or text.\n\n{\n \"status\": \"PASS\" | \"FAIL\",\n \"reason\": \"Brief explanation of why it failed (or 'Verified' if passed)\",\n}", + "batching": {} + }, + "type": "@n8n/n8n-nodes-langchain.chainLlm", + "typeVersion": 1.7, + "position": [ + 2624, + 240 + ], + "id": "df0e56d2-92ad-4ec9-b7f1-1044b3d9eb72", + "name": "QA Agent" + }, + { + "parameters": { + "sendTo": "me@ben.io", + "subject": "Anime Series List Update", + "message": "=This is an update that your Anime Audiobook Library, we've identified the following new books:\n\n{{ $json.email_summary }}\n\n", + "options": { + "appendAttribution": true, + "senderName": "n8n Anime Series List" + } + }, + "type": "n8n-nodes-base.gmail", + "typeVersion": 2.2, + "position": [ + 2272, + 16 + ], + "id": "6c328083-2cf9-4485-81bb-ba5b5fa3988e", + "name": "Send a message", + "webhookId": "d5463123-d50d-4eab-86c9-f71d18587d54", + "credentials": { + "gmailOAuth2": { + "id": "Os1ux3h3zFlC2XkG", + "name": "Gmail account" + } + } + }, + { + "parameters": { + "jsCode": "// Get all items from the previous node\nconst items = $input.all();\n\n// Create an HTML list of the books\nconst bookList = items.map(item => {\n return `
  • ${item.json.book_name} by ${item.json.author_folder}
  • `;\n}).join('');\n\n// Return an ARRAY containing the single summary item\nreturn [{\n json: {\n email_summary: `
      ${bookList}
    `,\n count: items.length\n }\n}];" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 2016, + 16 + ], + "id": "9657e35b-b89a-4d94-9ce2-41ba8ee70062", + "name": "Code in JavaScript" + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "c7daf60b-1af4-4764-a657-a1d66d30e1ee", + "leftValue": "={{ $json.book_name }}", + "rightValue": "PASS", + "operator": { + "type": "string", + "operation": "exists", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "options": {} + }, + "type": "n8n-nodes-base.if", + "typeVersion": 2.3, + "position": [ + 1744, + 64 + ], + "id": "d03ed1a1-1a8f-4863-97f8-2db150e77b68", + "name": "If" + }, + { + "parameters": { + "description": "Call this tool to lookup an Author and Book Name from Audible.", + "workflowId": { + "__rl": true, + "value": "Z_YHsJaf_pyFQR6e7VuLo", + "mode": "list", + "cachedResultUrl": "/workflow/Z_YHsJaf_pyFQR6e7VuLo", + "cachedResultName": "search_audible" + }, + "workflowInputs": { + "mappingMode": "defineBelow", + "value": { + "book-name": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('book-name', ``, 'string') }}", + "author": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('author', ``, 'string') }}" + }, + "matchingColumns": [], + "schema": [ + { + "id": "book-name", + "displayName": "book-name", + "required": false, + "defaultMatch": false, + "display": true, + "canBeUsedToMatch": true, + "type": "string", + "removed": false + }, + { + "id": "author", + "displayName": "author", + "required": false, + "defaultMatch": false, + "display": true, + "canBeUsedToMatch": true, + "type": "string", + "removed": false + }, + { + "id": "asin", + "displayName": "asin", + "required": false, + "defaultMatch": false, + "display": true, + "canBeUsedToMatch": true, + "type": "string", + "removed": true + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + } + }, + "type": "@n8n/n8n-nodes-langchain.toolWorkflow", + "typeVersion": 2.2, + "position": [ + 1776, + 608 + ], + "id": "81007b3e-3baa-4f95-bcb5-6e73fdcb91a0", + "name": "audible_search_book" + }, + { + "parameters": { + "description": "Call this tool to lookup an ASIN from Audible.", + "workflowId": { + "__rl": true, + "value": "Z_YHsJaf_pyFQR6e7VuLo", + "mode": "list", + "cachedResultUrl": "/workflow/Z_YHsJaf_pyFQR6e7VuLo", + "cachedResultName": "search_audible" + }, + "workflowInputs": { + "mappingMode": "defineBelow", + "value": { + "asin": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('asin', ``, 'string') }}" + }, + "matchingColumns": [], + "schema": [ + { + "id": "book-name", + "displayName": "book-name", + "required": false, + "defaultMatch": false, + "display": true, + "canBeUsedToMatch": true, + "type": "string", + "removed": true + }, + { + "id": "author", + "displayName": "author", + "required": false, + "defaultMatch": false, + "display": true, + "canBeUsedToMatch": true, + "type": "string", + "removed": true + }, + { + "id": "asin", + "displayName": "asin", + "required": false, + "defaultMatch": false, + "display": true, + "canBeUsedToMatch": true, + "type": "string", + "removed": false + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + } + }, + "type": "@n8n/n8n-nodes-langchain.toolWorkflow", + "typeVersion": 2.2, + "position": [ + 1600, + 608 + ], + "id": "dd7f49fd-2bd1-4e06-9402-5087db1c3a1e", + "name": "audible_lookup_asin" + } + ], + "connections": { + "Code": { + "main": [ + [ + { + "index": 0, + "node": "Filter Existing", + "type": "main" + } + ] + ] + }, + "List General Folders": { + "main": [ + [ + { + "index": 0, + "node": "Code", + "type": "main" + }, + { + "node": "Select rows from a table", + "type": "main", + "index": 0 + } + ] + ] + }, + "Workflow Trigger": { + "main": [ + [ + { + "index": 0, + "node": "List General Folders", + "type": "main" + } + ] + ] + }, + "tavily_search": { + "ai_tool": [ + [ + { + "node": "Lookup Agent", + "type": "ai_tool", + "index": 0 + } + ] + ] + }, + "Lookup Agent": { + "main": [ + [ + { + "node": "Merge", + "type": "main", + "index": 0 + } + ] + ] + }, + "Loop Over Items": { + "main": [ + [ + { + "node": "If", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Lookup Agent", + "type": "main", + "index": 0 + }, + { + "node": "Merge", + "type": "main", + "index": 1 + } + ] + ] + }, + "google_search": { + "ai_tool": [ + [ + { + "node": "Lookup Agent", + "type": "ai_tool", + "index": 0 + } + ] + ] + }, + "Filter Unwanted": { + "main": [ + [ + { + "node": "Loop Over Items", + "type": "main", + "index": 0 + } + ] + ] + }, + "Merge": { + "main": [ + [ + { + "node": "QA Agent", + "type": "main", + "index": 0 + } + ] + ] + }, + "verified": { + "main": [ + [ + { + "node": "Insert or update rows in a table", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Formatting", + "type": "main", + "index": 0 + } + ] + ] + }, + "Insert or update rows in a table": { + "main": [ + [ + { + "node": "Formatting", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Formatting", + "type": "main", + "index": 0 + } + ] + ] + }, + "Formatting": { + "main": [ + [ + { + "node": "Loop Over Items", + "type": "main", + "index": 0 + } + ] + ] + }, + "Structured Output Parser": { + "ai_outputParser": [ + [ + { + "node": "Lookup Agent", + "type": "ai_outputParser", + "index": 0 + } + ] + ] + }, + "Filter Existing": { + "main": [ + [ + { + "node": "Filter Unwanted", + "type": "main", + "index": 0 + } + ] + ] + }, + "Ollama - gpt-oss:20b - Validator": { + "ai_languageModel": [ + [ + { + "node": "Structured Output Parser", + "type": "ai_languageModel", + "index": 0 + }, + { + "node": "QA Agent", + "type": "ai_languageModel", + "index": 0 + } + ] + ] + }, + "Schedule Trigger": { + "main": [ + [ + { + "node": "List General Folders", + "type": "main", + "index": 0 + } + ] + ] + }, + "Select rows from a table": { + "main": [ + [ + { + "node": "Filter Existing", + "type": "main", + "index": 1 + } + ] + ] + }, + "brave_search": { + "ai_tool": [ + [ + { + "node": "Lookup Agent", + "type": "ai_tool", + "index": 0 + } + ] + ] + }, + "OpenAI Chat Model": { + "ai_languageModel": [ + [ + { + "node": "Lookup Agent", + "type": "ai_languageModel", + "index": 0 + } + ] + ] + }, + "QA Agent": { + "main": [ + [ + { + "node": "verified", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code in JavaScript": { + "main": [ + [ + { + "node": "Send a message", + "type": "main", + "index": 0 + } + ] + ] + }, + "If": { + "main": [ + [ + { + "node": "Code in JavaScript", + "type": "main", + "index": 0 + } + ] + ] + }, + "audible_search_book": { + "ai_tool": [ + [ + { + "node": "Lookup Agent", + "type": "ai_tool", + "index": 0 + } + ] + ] + }, + "audible_lookup_asin": { + "ai_tool": [ + [ + { + "node": "Lookup Agent", + "type": "ai_tool", + "index": 0 + } + ] + ] + } + }, + "settings": { + "executionOrder": "v1", + "callerPolicy": "workflowsFromSameOwner", + "availableInMCP": false + }, + "triggerCount": 1, + "versionId": "411aadb2-7f50-46cd-be00-401eef31dfab", + "owner": { + "type": "personal", + "projectId": "FeLO36wNUAcn61Wj", + "projectName": "Ben W ", + "personalEmail": "admin@ben.io" + }, + "parentFolderId": "6tDyZCwqELStb6Ik", + "isArchived": false +} \ No newline at end of file diff --git a/workflows/J3uKCCbSuQ1fdJkC.json b/workflows/J3uKCCbSuQ1fdJkC.json new file mode 100644 index 0000000..7e532c6 --- /dev/null +++ b/workflows/J3uKCCbSuQ1fdJkC.json @@ -0,0 +1,273 @@ +{ + "id": "J3uKCCbSuQ1fdJkC", + "name": "Audible Token Refresh", + "nodes": [ + { + "parameters": { + "rule": { + "interval": [ + { + "field": "hours" + } + ] + } + }, + "id": "schedule-trigger", + "name": "Schedule Trigger", + "position": [ + 0, + 0 + ], + "type": "n8n-nodes-base.scheduleTrigger", + "typeVersion": 1.1 + }, + { + "parameters": { + "operation": "executeQuery", + "query": "SELECT * FROM audible_credentials ORDER BY updated_at DESC LIMIT 1;", + "additionalFields": {} + }, + "id": "get-credentials", + "name": "Get Credentials", + "position": [ + 208, + 0 + ], + "type": "n8n-nodes-base.postgres", + "typeVersion": 1, + "credentials": { + "postgres": { + "id": "9grzZwW7Br6SzdV8", + "name": "n8n-media" + } + } + }, + { + "parameters": { + "jsCode": "// Encode raw_data to base64 for safe shell passing\nconst item = items[0];\nconst rawData = item.json.raw_data;\nconst base64Data = Buffer.from(JSON.stringify(rawData)).toString('base64');\nitem.json.creds_base64 = base64Data;\nreturn [item];" + }, + "id": "prepare-credentials", + "name": "Prepare Credentials", + "position": [ + 400, + 0 + ], + "type": "n8n-nodes-base.code", + "typeVersion": 2 + }, + { + "parameters": { + "jsCode": "return JSON.parse($input.first().json.stdout);" + }, + "id": "parse-output", + "name": "Parse Output", + "position": [ + 800, + 0 + ], + "type": "n8n-nodes-base.code", + "typeVersion": 2 + }, + { + "parameters": { + "authentication": "privateKey", + "command": "=/home/b3nw/.local/bin/uv run setup_auth.py -json", + "cwd": "/home/b3nw/projects/media/audible-script" + }, + "type": "n8n-nodes-base.ssh", + "typeVersion": 1, + "position": [ + 608, + 0 + ], + "id": "f858cc65-d12f-4b4f-8d49-03ba2a332b1c", + "name": "Execute a command", + "credentials": { + "sshPrivateKey": { + "id": "S2dcVMjrpg0I0kdV", + "name": "vscode-dev.local.ben.io" + } + } + }, + { + "parameters": { + "operation": "update", + "schema": { + "__rl": true, + "mode": "list", + "value": "public" + }, + "table": { + "__rl": true, + "value": "audible_credentials", + "mode": "list", + "cachedResultName": "audible_credentials" + }, + "columns": { + "mappingMode": "defineBelow", + "value": { + "access_token": "={{ $json.auth.access_token }}", + "refresh_token": "={{ $json.auth.refresh_token }}", + "id": 1, + "raw_data": "={{ $json.auth }}", + "expires_at": "={{ DateTime.fromSeconds($json.auth.expires).setZone('UTC').toFormat('yyyy-MM-dd HH:mm:ss.SSS') }}+00", + "updated_at": "={{ $now.setZone('UTC').toFormat('yyyy-MM-dd HH:mm:ss.SSS') }}+00" + }, + "matchingColumns": [ + "id" + ], + "schema": [ + { + "id": "id", + "displayName": "id", + "required": false, + "defaultMatch": true, + "display": true, + "type": "number", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "access_token", + "displayName": "access_token", + "required": true, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "refresh_token", + "displayName": "refresh_token", + "required": true, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "expires_at", + "displayName": "expires_at", + "required": false, + "defaultMatch": false, + "display": true, + "type": "dateTime", + "canBeUsedToMatch": true + }, + { + "id": "raw_data", + "displayName": "raw_data", + "required": false, + "defaultMatch": false, + "display": true, + "type": "object", + "canBeUsedToMatch": true + }, + { + "id": "updated_at", + "displayName": "updated_at", + "required": false, + "defaultMatch": false, + "display": true, + "type": "dateTime", + "canBeUsedToMatch": true + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {} + }, + "type": "n8n-nodes-base.postgres", + "typeVersion": 2.6, + "position": [ + 976, + 0 + ], + "id": "be8ac7c8-c6ee-464c-8483-421f0959dae6", + "name": "Update rows in a table", + "credentials": { + "postgres": { + "id": "9grzZwW7Br6SzdV8", + "name": "n8n-media" + } + } + } + ], + "connections": { + "Get Credentials": { + "main": [ + [ + { + "index": 0, + "node": "Prepare Credentials", + "type": "main" + } + ] + ] + }, + "Parse Output": { + "main": [ + [ + { + "node": "Update rows in a table", + "type": "main", + "index": 0 + } + ] + ] + }, + "Prepare Credentials": { + "main": [ + [ + { + "node": "Execute a command", + "type": "main", + "index": 0 + } + ] + ] + }, + "Schedule Trigger": { + "main": [ + [ + { + "index": 0, + "node": "Get Credentials", + "type": "main" + } + ] + ] + }, + "Execute a command": { + "main": [ + [ + { + "node": "Parse Output", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "settings": { + "executionOrder": "v1", + "saveDataErrorExecution": "all", + "saveDataSuccessExecution": "all", + "saveManualExecutions": true, + "saveExecutionProgress": true, + "callerPolicy": "workflowsFromSameOwner", + "availableInMCP": false + }, + "triggerCount": 1, + "versionId": "efe8c6e8-c102-4086-ace6-89dc1ac2a381", + "owner": { + "type": "personal", + "projectId": "FeLO36wNUAcn61Wj", + "projectName": "Ben W ", + "personalEmail": "admin@ben.io" + }, + "parentFolderId": "of8yoeyjjIAhYdnQ", + "isArchived": false +} \ No newline at end of file diff --git a/workflows/KGpJklJeOqX8c3VG.json b/workflows/KGpJklJeOqX8c3VG.json new file mode 100644 index 0000000..959001c --- /dev/null +++ b/workflows/KGpJklJeOqX8c3VG.json @@ -0,0 +1,122 @@ +{ + "id": "KGpJklJeOqX8c3VG", + "name": "Daily Tasks", + "nodes": [ + { + "parameters": { + "rule": { + "interval": [ + { + "triggerAtHour": 6 + } + ] + } + }, + "type": "n8n-nodes-base.scheduleTrigger", + "typeVersion": 1.3, + "position": [ + -656, + -32 + ], + "id": "7c870564-f742-41b4-99e7-219e0ed184e9", + "name": "Schedule Trigger" + }, + { + "parameters": { + "promptType": "define", + "text": "Review my open tasks in each list and prepare an HTML summary of the tasks in order of their due date. Include a web link to each task.", + "options": { + "systemMessage": "You are a helpful assistant. You have access to the users Google Tasks via tools.", + "returnIntermediateSteps": false + } + }, + "type": "@n8n/n8n-nodes-langchain.agent", + "typeVersion": 3, + "position": [ + -448, + -32 + ], + "id": "5019e966-7659-43af-90c2-cb10a560874d", + "name": "AI Agent" + }, + { + "parameters": { + "model": "gpt-oss:20b", + "options": {} + }, + "type": "@n8n/n8n-nodes-langchain.lmChatOllama", + "typeVersion": 1, + "position": [ + -448, + 176 + ], + "id": "89002475-cd04-4451-9b16-a7ea316e5ee4", + "name": "Ollama Chat Model", + "credentials": { + "ollamaApi": { + "id": "l1g3pgQImkg18AzR", + "name": "Ollama account" + } + } + }, + { + "parameters": {}, + "type": "@n8n/n8n-nodes-langchain.toolCode", + "typeVersion": 1.3, + "position": [ + -208, + 272 + ], + "id": "2936a35b-ade5-4dca-b82d-ee5418ca5add", + "name": "Code Tool" + } + ], + "connections": { + "Schedule Trigger": { + "main": [ + [ + { + "node": "AI Agent", + "type": "main", + "index": 0 + } + ] + ] + }, + "Ollama Chat Model": { + "ai_languageModel": [ + [ + { + "node": "AI Agent", + "type": "ai_languageModel", + "index": 0 + } + ] + ] + }, + "Code Tool": { + "ai_tool": [ + [ + { + "node": "AI Agent", + "type": "ai_tool", + "index": 0 + } + ] + ] + } + }, + "settings": { + "executionOrder": "v1" + }, + "triggerCount": 0, + "versionId": "d0763a35-96bb-40a5-9c09-f9f4199cb231", + "owner": { + "type": "personal", + "projectId": "FeLO36wNUAcn61Wj", + "projectName": "Ben W ", + "personalEmail": "admin@ben.io" + }, + "parentFolderId": "OJ2UfPNUOAOHlllh", + "isArchived": true +} \ No newline at end of file diff --git a/workflows/KanydpqJrpsAGDCMqkpgD.json b/workflows/KanydpqJrpsAGDCMqkpgD.json new file mode 100644 index 0000000..41e26f1 --- /dev/null +++ b/workflows/KanydpqJrpsAGDCMqkpgD.json @@ -0,0 +1,98 @@ +{ + "id": "KanydpqJrpsAGDCMqkpgD", + "name": "Outline Daily Notes", + "nodes": [ + { + "parameters": { + "rule": { + "interval": [ + { + "triggerAtHour": 4 + } + ] + } + }, + "type": "n8n-nodes-base.scheduleTrigger", + "typeVersion": 1.3, + "position": [ + 0, + 0 + ], + "id": "5d2e1844-e4ba-4e6a-92fa-86c3ded88897", + "name": "Schedule Trigger" + }, + { + "parameters": { + "authentication": "privateKey", + "command": "/home/b3nw/projects/personal/daily-ideas/run-daily.sh", + "cwd": "/home/b3nw/projects/personal/daily-ideas" + }, + "type": "n8n-nodes-base.ssh", + "typeVersion": 1, + "position": [ + 208, + 0 + ], + "id": "6cb707c1-1ee5-409b-84cb-3ed810136c70", + "name": "Execute a command", + "credentials": { + "sshPrivateKey": { + "id": "S2dcVMjrpg0I0kdV", + "name": "vscode-dev.local.ben.io" + } + } + }, + { + "parameters": { + "language": "pythonNative", + "pythonCode": "import json\nimport re\n\n# Get stdout from Execute Command node\nstdout_content = _items[0][\"json\"].get(\"stdout\", \"\")\n\n# Parse NDJSON\nlines = stdout_content.strip().split(\"\\n\")\n\n# Collect all events by type\nresult = {\n \"session_id\": None,\n \"status\": \"unknown\",\n \"created_document\": None,\n \"tool_calls\": [],\n \"text_responses\": [],\n \"steps\": [],\n \"total_cost\": 0.0,\n \"total_tokens\": {\n \"input\": 0,\n \"output\": 0,\n \"reasoning\": 0\n }\n}\n\nfor line in lines:\n try:\n event = json.loads(line)\n event_type = event.get(\"type\")\n \n # Capture session ID\n if not result[\"session_id\"]:\n result[\"session_id\"] = event.get(\"sessionID\")\n \n part = event.get(\"part\", {})\n \n if event_type == \"text\":\n result[\"text_responses\"].append({\n \"id\": part.get(\"id\"),\n \"text\": part.get(\"text\", \"\"),\n \"timestamp\": event.get(\"timestamp\")\n })\n \n elif event_type == \"tool_use\":\n tool_name = part.get(\"tool\", \"\")\n state = part.get(\"state\", {})\n \n tool_call = {\n \"tool\": tool_name,\n \"call_id\": part.get(\"callID\"),\n \"status\": state.get(\"status\"),\n \"input\": state.get(\"input\"),\n \"timestamp\": event.get(\"timestamp\")\n }\n \n # Parse output if completed\n if state.get(\"status\") == \"completed\":\n output_str = state.get(\"output\", \"{}\")\n try:\n tool_call[\"output\"] = json.loads(output_str)\n except:\n tool_call[\"output\"] = output_str\n \n result[\"tool_calls\"].append(tool_call)\n \n # Check for document creation\n if \"create_document\" in tool_name and state.get(\"status\") == \"completed\":\n try:\n output_data = json.loads(state.get(\"output\", \"{}\"))\n if \"data\" in output_data and \"id\" in output_data[\"data\"]:\n result[\"created_document\"] = output_data[\"data\"]\n result[\"status\"] = \"document_created\"\n except:\n pass\n \n elif event_type == \"step_finish\":\n step_info = {\n \"id\": part.get(\"id\"),\n \"reason\": part.get(\"reason\"),\n \"cost\": part.get(\"cost\", 0),\n \"tokens\": part.get(\"tokens\", {}),\n \"timestamp\": event.get(\"timestamp\")\n }\n result[\"steps\"].append(step_info)\n \n # Accumulate costs and tokens\n result[\"total_cost\"] += part.get(\"cost\", 0)\n tokens = part.get(\"tokens\", {})\n result[\"total_tokens\"][\"input\"] += tokens.get(\"input\", 0)\n result[\"total_tokens\"][\"output\"] += tokens.get(\"output\", 0)\n result[\"total_tokens\"][\"reasoning\"] += tokens.get(\"reasoning\", 0)\n \n except json.JSONDecodeError:\n continue\n\n# Determine final status\nif result[\"created_document\"]:\n result[\"status\"] = \"document_created\"\nelif result[\"tool_calls\"]:\n # Check if any tool calls failed\n failed = [t for t in result[\"tool_calls\"] if t.get(\"status\") != \"completed\"]\n if failed:\n result[\"status\"] = \"tool_error\"\n else:\n result[\"status\"] = \"tools_completed\"\nelif result[\"text_responses\"]:\n result[\"status\"] = \"text_response\"\n\n# Combine all text for summary\nresult[\"full_text\"] = \"\\n\\n\".join([t[\"text\"] for t in result[\"text_responses\"]])\n\nreturn result" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 416, + 0 + ], + "id": "74be33c5-e738-4b83-9bd6-9ae19186f86a", + "name": "Code in Python (Native)" + } + ], + "connections": { + "Schedule Trigger": { + "main": [ + [ + { + "node": "Execute a command", + "type": "main", + "index": 0 + } + ] + ] + }, + "Execute a command": { + "main": [ + [ + { + "node": "Code in Python (Native)", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "settings": { + "executionOrder": "v1", + "availableInMCP": false + }, + "triggerCount": 1, + "versionId": "4a28d8c0-0420-4555-8019-4b972d63cdac", + "owner": { + "type": "personal", + "projectId": "FeLO36wNUAcn61Wj", + "projectName": "Ben W ", + "personalEmail": "admin@ben.io" + }, + "parentFolderId": "OJ2UfPNUOAOHlllh", + "isArchived": false +} \ No newline at end of file diff --git a/workflows/Mdopqz1Tq0OHDFq1.json b/workflows/Mdopqz1Tq0OHDFq1.json new file mode 100644 index 0000000..c8ec81b --- /dev/null +++ b/workflows/Mdopqz1Tq0OHDFq1.json @@ -0,0 +1,284 @@ +{ + "id": "Mdopqz1Tq0OHDFq1", + "name": "ben.io-task agent", + "nodes": [ + { + "parameters": { + "workflowInputs": { + "values": [ + { + "name": "request" + } + ] + } + }, + "type": "n8n-nodes-base.executeWorkflowTrigger", + "typeVersion": 1.1, + "position": [ + -752, + 16 + ], + "id": "781db6ec-9be3-4db2-a194-1ef21b550f42", + "name": "When Executed by Another Workflow" + }, + { + "parameters": { + "promptType": "define", + "text": "=Generate and send my daily task briefing email based on my current open tasks.", + "needsFallback": true, + "options": { + "systemMessage": "You are an executive assistant responsible for generating a daily task briefing.\n\n## Data Processing Rules\n1. **Fetch Data**: Always call both `GTasks: Get All Tasklists` and `GTasks: Get All Tasks`.\n2. **Map Categories**: \n - You must link Tasks to their List Name. \n - Extract the List ID from the task's `selfLink` attribute (the string between `/lists/` and `/tasks/`).\n - Match this ID to the `id` from the Tasklists output to get the human-readable Category Name (e.g., \"Finance\", \"House\").\n3. **Determine Priority**:\n - **High**: Due date is today, in the past (overdue), or status is 'needsAction' with a due date today/past.\n - **Normal**: Due date is within the next 7 days.\n - **Low**: No due date or due date is > 7 days away.\n - *Exclude tasks with status 'completed'.*\n\n## Output Formatting\n1. **Style**: Use simple, semantic HTML only. No CSS or `