initial commit

This commit is contained in:
Florian Krebs
2026-05-04 20:49:45 +00:00
parent 7d205a68e4
commit 18d2360ad9
14 changed files with 1811 additions and 3 deletions
@@ -0,0 +1,127 @@
{
"name": "Phase 1.1: Nextcloud Lists Discovery",
"description": "Discovers and maps Nextcloud task list IDs (Work, Personal, Shopping, Wishes) for environment configuration",
"nodes": [
{
"parameters": {},
"name": "Start",
"type": "n8n-nodes-base.start",
"typeVersion": 1,
"position": [50, 50]
},
{
"parameters": {
"method": "GET",
"url": "{{ $env.NC_HOST }}/remote.php/dav/lists/",
"authentication": "basicAuth",
"basicAuth": {
"user": "{{ $env.NC_USERNAME }}",
"password": "{{ $env.NC_APP_PASSWORD }}"
},
"responseFormat": "text"
},
"name": "Fetch Nextcloud Lists (WebDAV)",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 3,
"position": [250, 50],
"onError": "continueErrorOutput"
},
{
"parameters": {
"jsCode": "// Parse XML response from WebDAV\nconst xmlText = $input.item.json.body || '';\n\n// Extract list names and IDs from XML\nconst listRegex = /<d:href>(\\/remote\\.php\\/dav\\/lists\\/[^/]+\\/)<\\/d:href>[\\s\\S]*?<d:displayname>([^<]+)<\\/d:displayname>/g;\nconst lists = [];\nlet match;\n\nwhile ((match = listRegex.exec(xmlText)) !== null) {\n const href = match[1];\n const displayname = match[2];\n const listId = href.split('/').filter(p => p)[3]; // Extract ID from path\n lists.push({\n name: displayname,\n id: listId,\n url: href\n });\n}\n\n// Identify special lists by name patterns\nconst result = {\n discovery_timestamp: new Date().toISOString(),\n lists_found: lists,\n mapping: {\n work: lists.find(l => l.name.toLowerCase().includes('work'))?.id || 'NOT_FOUND',\n personal: lists.find(l => l.name.toLowerCase().includes('personal'))?.id || 'NOT_FOUND',\n shopping: lists.find(l => l.name.toLowerCase().includes('shopping'))?.id || 'NOT_FOUND',\n wishes: lists.find(l => l.name.toLowerCase().includes('wishes') || l.name.toLowerCase().includes('want'))?.id || 'NOT_FOUND'\n }\n};\n\nreturn result;"
},
"name": "Parse WebDAV Response",
"type": "n8n-nodes-base.code",
"typeVersion": 1,
"position": [450, 50]
},
{
"parameters": {
"jsCode": "// Generate .env snippet for configuration\nconst mapping = $input.item.json.mapping;\nconst timestamp = new Date().toISOString();\n\nconst envSnippet = `# NEXA Nextcloud Lists - Discovered on ${timestamp}\nNC_LIST_ID_WORK=${mapping.work}\nNC_LIST_ID_PERSONAL=${mapping.personal}\nNC_LIST_ID_SHOPPING=${mapping.shopping}\nNC_LIST_ID_WISHES=${mapping.wishes}`;\n\nreturn {\n env_snippet: envSnippet,\n discovered_lists: $input.item.json.lists_found,\n ready_for_deployment: Object.values(mapping).every(v => v !== 'NOT_FOUND')\n};"
},
"name": "Generate .env Configuration",
"type": "n8n-nodes-base.code",
"typeVersion": 1,
"position": [650, 50]
},
{
"parameters": {
"method": "POST",
"url": "{{ $env.MEMOS_HOST }}/api/v1/memos",
"authentication": "headerAuth",
"headerAuth": {
"Authorization": "Bearer {{ $env.MEMOS_API_KEY }}"
},
"sendBody": true,
"bodyParametersUi": "json",
"body": "{\n \"content\": \"🔍 **Nexa Discovery Report**\\n\\nNextcloud Lists gefunden:\\n\\n- **Work:** {{ $input.item.json.env_snippet.split('=')[1].split('\\\\n')[0] }}\\n- **Personal:** {{ $input.item.json.env_snippet.split('=')[2].split('\\\\n')[0] }}\\n- **Shopping:** {{ $input.item.json.env_snippet.split('=')[3].split('\\\\n')[0] }}\\n- **Wishes:** {{ $input.item.json.env_snippet.split('=')[4].split('\\\\n')[0] }}\\n\\nBitte diese IDs in .env eintragen.\",\n \"visibility\": \"PRIVATE\"\n}"
},
"name": "Post Report to Memos",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 3,
"position": [850, 50],
"onError": "continueErrorOutput"
},
{
"parameters": {
"fromFile": true,
"fileName": "discovery_result.json"
},
"name": "Output Discovery Result",
"type": "n8n-nodes-base.set",
"typeVersion": 2,
"position": [1050, 50]
}
],
"connections": {
"Start": {
"main": [
[
{
"node": "Fetch Nextcloud Lists (WebDAV)",
"type": "main",
"index": 0
}
]
]
},
"Fetch Nextcloud Lists (WebDAV)": {
"main": [
[
{
"node": "Parse WebDAV Response",
"type": "main",
"index": 0
}
]
]
},
"Parse WebDAV Response": {
"main": [
[
{
"node": "Generate .env Configuration",
"type": "main",
"index": 0
}
]
]
},
"Generate .env Configuration": {
"main": [
[
{
"node": "Post Report to Memos",
"type": "main",
"index": 0
},
{
"node": "Output Discovery Result",
"type": "main",
"index": 0
}
]
]
}
}
}
@@ -0,0 +1,58 @@
{
"name": "Phase 1.2: Memos Bridge - Context Classifier & Task Router",
"description": "Receives Memos, classifies context (work/personal), and routes to appropriate Nextcloud list via SAIA",
"nodes": [
{
"parameters": {
"path": "memos"
},
"name": "Memos Webhook Trigger",
"type": "n8n-nodes-base.webhook",
"typeVersion": 1,
"position": [50, 50]
},
{
"parameters": {
"jsCode": "// Extract and validate Memos webhook payload\nconst payload = $input.item.json;\n\n// Structure expected from Memos webhook\nconst memo = {\n id: payload.memo?.id || payload.id,\n content: payload.memo?.content || payload.content || '',\n createdAt: payload.memo?.createdAt || payload.createdAt,\n archived: payload.memo?.archived || false,\n // Detect if it's a task (contains - [ ] or #todo)\n isTask: /(- \\[ \\]|-\\s\\[ \\])|(#todo)/i.test(payload.memo?.content || payload.content || ''),\n isPriority: /(#urgent|#important|!important)/i.test(payload.memo?.content || payload.content || '')\n};\n\n// Ignore archived memos\nif (memo.archived) {\n throw new Error('Skipping archived memo');\n}\n\n// Only process if it contains task marker or #todo\nif (!memo.isTask) {\n throw new Error('Memo is not marked as task (missing - [ ] or #todo)');\n}\n\nreturn {\n memo: memo,\n shouldProcess: true,\n timestamp: new Date().toISOString()\n};"
},
"name": "Validate Memo Payload",
"type": "n8n-nodes-base.code",
"typeVersion": 1,
"position": [250, 50]
},
{
"parameters": {
"method": "POST",
"url": "{{ $env.SAIA_HOST }}/v1/chat/completions",
"authentication": "headerAuth",
"headerAuth": {
"Authorization": "Bearer {{ $env.SAIA_API_KEY }}"
},
"sendBody": true,
"bodyParametersUi": "json",
"body": "{\n \"model\": \"{{ $env.SAIA_MODEL_CLASSIFICATION }}\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": \"You are Nexa, a context classifier. Analyze the following task memo and classify it as either 'work' or 'personal'. Consider:\\n- Work context: Professional projects, client work, coding, architecture, development\\n- Personal context: Household, shopping, hobbies, health, leisure\\n\\nRespond with ONLY valid JSON: {\\\"context\\\": \\\"work\\\" | \\\"personal\\\", \\\"urgency\\\": 1-5, \\\"summary\\\": \\\"one-line summary\\\", \\\"confidence\\\": 0.0-1.0}\"\n },\n {\n \"role\": \"user\",\n \"content\": \"{{ $input.item.json.memo.content }}\"\n }\n ],\n \"temperature\": 0.3,\n \"max_tokens\": 200\n}"
},
"name": "SAIA: Classify Context",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 3,
"position": [450, 50]
},
{
"parameters": {
"jsCode": "// Parse SAIA response\nconst response = $input.item.json;\nconst content = response.choices?.[0]?.message?.content || '{}';\n\nlet classification;\ntry {\n // Try to extract JSON from response\n const jsonMatch = content.match(/\\{[^{}]*\\}/);\n classification = JSON.parse(jsonMatch ? jsonMatch[0] : content);\n} catch (e) {\n console.error('Failed to parse SAIA response:', content);\n classification = { context: 'personal', confidence: 0 };\n}\n\nreturn {\n raw_response: content,\n classification: classification,\n timestamp: new Date().toISOString()\n};"
},
"name": "Parse Classification",
"type": "n8n-nodes-base.code",
"typeVersion": 1,
"position": [650, 50]
},
{
"parameters": {
"conditions": {\n "options": [\n {\n "condition": "nodeOutputProperty",\n "value1": "={{ $node[\\\"Parse Classification\\\"].json.classification.context }}",\n "value2": "work"\n },\n {\n "condition": "nodeOutputProperty",\n "value1": "={{ $node[\\\"Parse Classification\\\"].json.classification.context }}",\n "value2": "personal"\n }\n ]\n }\n },\n "name": "Route by Context",\n "type": "n8n-nodes-base.switch",
n "typeVersion": 1,\n "position": [850, 50]\n },\n {\n "parameters": {\n "method": "POST",\n "url": "{{ $env.NC_HOST }}/remote.php/dav/lists/{{ $env.NC_LIST_ID_WORK }}/\",\n "authentication": "basicAuth",\n "basicAuth": {\n "user": "{{ $env.NC_USERNAME }}\",\n "password": "{{ $env.NC_APP_PASSWORD }}\"\n },\n "sendBody": true,\n "bodyParametersUi": "raw",\n "body": "BEGIN:VCALENDAR\\nVERSION:2.0\\nPRODID:-//Nexa//v1.0//EN\\nBEGIN:VTODO\\nUID:memo-{{ $node[\\\"Validate Memo Payload\\\"].json.memo.id }}-work\\nDTSTART:{{ $node[\\\"Validate Memo Payload\\\"].json.memo.createdAt }}\\nDTSTAMP:{{ now().toISOString() }}\\nSUMMARY:{{ ($node[\\\"Parse Classification\\\"].json.classification.summary || $node[\\\"Validate Memo Payload\\\"].json.memo.content.substring(0, 50)).replace(/\\\"/g, \\\"\\\") }}\\nDESCRIPTION:{{ $node[\\\"Validate Memo Payload\\\"].json.memo.content }}\\nPRIORITY:{{ Math.round((6 - $node[\\\"Parse Classification\\\"].json.classification.urgency)) }}\\nSTATUS:NEEDS-ACTION\\nEND:VTODO\\nEND:VCALENDAR\"\n },\n "name": "Create Task - Work List",\n "type": "n8n-nodes-base.httpRequest",\n "typeVersion": 3,\n "position": [1050, 50],\n "onError": "continueErrorOutput"\n },\n {\n "parameters": {\n "method": "POST",\n "url": "{{ $env.NC_HOST }}/remote.php/dav/lists/{{ $env.NC_LIST_ID_PERSONAL }}/\",\n "authentication": "basicAuth",\n "basicAuth": {\n "user": "{{ $env.NC_USERNAME }}\",\n "password": "{{ $env.NC_APP_PASSWORD }}\"\n },\n "sendBody": true,\n "bodyParametersUi": "raw",\n "body": "BEGIN:VCALENDAR\\nVERSION:2.0\\nPRODID:-//Nexa//v1.0//EN\\nBEGIN:VTODO\\nUID:memo-{{ $node[\\\"Validate Memo Payload\\\"].json.memo.id }}-personal\\nDTSTART:{{ $node[\\\"Validate Memo Payload\\\"].json.memo.createdAt }}\\nDTSTAMP:{{ now().toISOString() }}\\nSUMMARY:{{ ($node[\\\"Parse Classification\\\"].json.classification.summary || $node[\\\"Validate Memo Payload\\\"].json.memo.content.substring(0, 50)).replace(/\\\"/g, \\\"\\\") }}\\nDESCRIPTION:{{ $node[\\\"Validate Memo Payload\\\"].json.memo.content }}\\nPRIORITY:{{ Math.round((6 - $node[\\\"Parse Classification\\\"].json.classification.urgency)) }}\\nSTATUS:NEEDS-ACTION\\nEND:VTODO\\nEND:VCALENDAR\"\n },\n "name": "Create Task - Personal List",\n "type": "n8n-nodes-base.httpRequest",\n "typeVersion": 3,\n "position": [1050, 200],\n "onError": "continueErrorOutput"\n },\n {\n "parameters": {\n "jsCode": "// Create feedback message based on routing decision\nconst context = $node[\\\"Parse Classification\\\"].json.classification.context;\nconst urgency = $node[\\\"Parse Classification\\\"].json.classification.urgency;\nconst listName = context === 'work' ? 'Work' : 'Personal';\nconst emoji = context === 'work' ? '💼' : '🏠';\n\nreturn {\n feedback_content: `${emoji} Task routed to ${listName} list (Urgency: ${urgency}/5)`,\n classification: $node[\\\"Parse Classification\\\"].json.classification,\n memo_id: $node[\\\"Validate Memo Payload\\\"].json.memo.id\n};"
},
"name": "Prepare Feedback",
"type": "n8n-nodes-base.code",
"typeVersion": 1,
"position": [1250, 50]
},\n {\n "parameters": {\n "method": "PATCH",\n "url": "{{ $env.MEMOS_HOST }}/api/v1/memos/{{ $node[\\\"Validate Memo Payload\\\"].json.memo.id }}\",\n "authentication": "headerAuth",\n "headerAuth": {\n "Authorization": "Bearer {{ $env.MEMOS_API_KEY }}\"\n },\n "sendBody": true,\n "bodyParametersUi": "json",\n "body": "{\n \\\"content\\\": \\\"{{ $node[\\\"Prepare Feedback\\\"].json.feedback_content }}\\n\\n_Processed by Nexa at {{ now().toLocaleString() }}_\\\"\n}"\n },\n "name": "Post Feedback to Memos",\n "type": "n8n-nodes-base.httpRequest",\n "typeVersion": 3,\n "position": [1450, 50],\n "onError": "continueErrorOutput"\n }\n ],\n "connections": {\n "Memos Webhook Trigger": {\n "main": [\n [\n {\n "node": "Validate Memo Payload",\n "type": "main",\n "index": 0\n }\n ]\n ]\n },\n "Validate Memo Payload": {\n "main": [\n [\n {\n "node": "SAIA: Classify Context",\n "type": "main",\n "index": 0\n }\n ]\n ]\n },\n "SAIA: Classify Context": {\n "main": [\n [\n {\n "node": "Parse Classification",\n "type": "main",\n "index": 0\n }\n ]\n ]\n },\n "Parse Classification": {\n "main": [\n [\n {\n "node": "Route by Context",\n "type": "main",\n "index": 0\n }\n ]\n ]\n },\n "Route by Context": {\n "main": [\n [ { "node": "Create Task - Work List", "type": "main", "index": 0 } ],\n [ { "node": "Create Task - Personal List", "type": "main", "index": 0 } ]\n ]\n },\n "Create Task - Work List": {\n "main": [\n [\n {\n "node": "Prepare Feedback",\n "type": "main",\n "index": 0\n }\n ]\n ]\n },\n "Create Task - Personal List": {\n "main": [\n [\n {\n "node": "Prepare Feedback",\n "type": "main",\n "index": 0\n }\n ]\n ]\n },\n "Prepare Feedback": {\n "main": [\n [\n {\n "node": "Post Feedback to Memos",\n "type": "main",\n "index": 0\n }\n ]\n ]\n }\n }\n}\n
@@ -0,0 +1,143 @@
{
"name": "Phase 1.2: Memos Bridge - Context Classifier & Task Router",
"description": "Receives Memos webhook, classifies context (work/personal) via SAIA, routes to appropriate Nextcloud list",
"nodes": [
{
"parameters": {
"path": "memos"
},
"name": "Memos Webhook Trigger",
"type": "n8n-nodes-base.webhook",
"typeVersion": 1,
"position": [50, 50]
},
{
"parameters": {
"jsCode": "const payload = $input.item.json;\nconst memo = {\n id: payload.memo?.id || payload.id,\n content: payload.memo?.content || payload.content || '',\n createdAt: payload.memo?.createdAt || payload.createdAt,\n archived: payload.memo?.archived || false,\n isTask: /(- \\[ \\]|#todo)/i.test(payload.memo?.content || payload.content || ''),\n isPriority: /(#urgent|#important)/i.test(payload.memo?.content || payload.content || '')\n};\nif (memo.archived || !memo.isTask) {\n throw new Error('Memo not a task');\n}\nreturn { memo, timestamp: new Date().toISOString() };"
},
"name": "Validate Memo Payload",
"type": "n8n-nodes-base.code",
"typeVersion": 1,
"position": [250, 50]
},
{
"parameters": {
"method": "POST",
"url": "={{ $env.SAIA_HOST }}/v1/chat/completions",
"authentication": "headerAuth",
"headerAuth": {
"Authorization": "Bearer {{ $env.SAIA_API_KEY }}"
},
"sendBody": true,
"bodyParametersUi": "json",
"body": "{\n \"model\": \"{{ $env.SAIA_MODEL_CLASSIFICATION }}\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": \"Classify as work or personal. Respond: {context: work|personal, urgency: 1-5, summary: str}\"\n },\n {\n \"role\": \"user\",\n \"content\": \"{{ $input.item.json.memo.content }}\"\n }\n ],\n \"temperature\": 0.3\n}"
},
"name": "SAIA: Classify Context",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 3,
"position": [450, 50]
},
{
"parameters": {
"jsCode": "const content = $input.item.json.choices?.[0]?.message?.content || '{}';\nlet classification;\ntry {\n const jsonMatch = content.match(/\\{[^{}]*\\}/);\n classification = JSON.parse(jsonMatch?.[0] || content);\n} catch (e) {\n classification = { context: 'personal', urgency: 2 };\n}\nreturn { classification, raw: content };"
},
"name": "Parse Classification",
"type": "n8n-nodes-base.code",
"typeVersion": 1,
"position": [650, 50]
},
{
"parameters": {
"conditions": {
"options": [
{
"condition": "nodeOutputProperty",
"value1": "={{ $node[\"Parse Classification\"].json.classification.context }}",
"value2": "work"
},
{
"condition": "nodeOutputProperty",
"value1": "={{ $node[\"Parse Classification\"].json.classification.context }}",
"value2": "personal"
}
]
}
},
"name": "Route by Context",
"type": "n8n-nodes-base.switch",
"typeVersion": 1,
"position": [850, 50]
},
{
"parameters": {
"method": "POST",
"url": "={{ $env.NC_HOST }}/remote.php/dav/lists/{{ $env.NC_LIST_ID_WORK }}/",
"authentication": "basicAuth",
"basicAuth": {
"user": "={{ $env.NC_USERNAME }}",
"password": "={{ $env.NC_APP_PASSWORD }}"
},
"sendBody": true,
"bodyParametersUi": "raw",
"body": "BEGIN:VCALENDAR\nVERSION:2.0\nPRODID:-//Nexa//v1.0//EN\nBEGIN:VTODO\nUID:{{ $node[\"Validate Memo Payload\"].json.memo.id }}\nSUMMARY:{{ $node[\"Parse Classification\"].json.classification.summary || $node[\"Validate Memo Payload\"].json.memo.content.substring(0, 50) }}\nDESCRIPTION:{{ $node[\"Validate Memo Payload\"].json.memo.content }}\nSTATUS:NEEDS-ACTION\nEND:VTODO\nEND:VCALENDAR"
},
"name": "Create Task - Work List",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 3,
"position": [1050, 50]
},
{
"parameters": {
"method": "POST",
"url": "={{ $env.NC_HOST }}/remote.php/dav/lists/{{ $env.NC_LIST_ID_PERSONAL }}/",
"authentication": "basicAuth",
"basicAuth": {
"user": "={{ $env.NC_USERNAME }}",
"password": "={{ $env.NC_APP_PASSWORD }}"
},
"sendBody": true,
"bodyParametersUi": "raw",
"body": "BEGIN:VCALENDAR\nVERSION:2.0\nPRODID:-//Nexa//v1.0//EN\nBEGIN:VTODO\nUID:{{ $node[\"Validate Memo Payload\"].json.memo.id }}\nSUMMARY:{{ $node[\"Parse Classification\"].json.classification.summary || $node[\"Validate Memo Payload\"].json.memo.content.substring(0, 50) }}\nDESCRIPTION:{{ $node[\"Validate Memo Payload\"].json.memo.content }}\nSTATUS:NEEDS-ACTION\nEND:VTODO\nEND:VCALENDAR"
},
"name": "Create Task - Personal List",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 3,
"position": [1050, 200]
},
{
"parameters": {
"jsCode": "const context = $node[\"Parse Classification\"].json.classification.context;\nconst emoji = context === 'work' ? '💼' : '🏠';\nreturn { feedback: emoji + ' Task routed to ' + context + ' list', context };"
},
"name": "Prepare Feedback",
"type": "n8n-nodes-base.code",
"typeVersion": 1,
"position": [1250, 50]
}
],
"connections": {
"Memos Webhook Trigger": {
"main": [[{ "node": "Validate Memo Payload", "type": "main", "index": 0 }]]
},
"Validate Memo Payload": {
"main": [[{ "node": "SAIA: Classify Context", "type": "main", "index": 0 }]]
},
"SAIA: Classify Context": {
"main": [[{ "node": "Parse Classification", "type": "main", "index": 0 }]]
},
"Parse Classification": {
"main": [[{ "node": "Route by Context", "type": "main", "index": 0 }]]
},
"Route by Context": {
"main": [
[{ "node": "Create Task - Work List", "type": "main", "index": 0 }],
[{ "node": "Create Task - Personal List", "type": "main", "index": 0 }]
]
},
"Create Task - Work List": {
"main": [[{ "node": "Prepare Feedback", "type": "main", "index": 0 }]]
},
"Create Task - Personal List": {
"main": [[{ "node": "Prepare Feedback", "type": "main", "index": 0 }]]
}
}
}