From 7b6654f085f0da84a1f9f46855fc419077e06d83 Mon Sep 17 00:00:00 2001 From: Shaela Bello Date: Thu, 12 Mar 2026 04:10:51 -0400 Subject: [PATCH 1/3] chore(n8n): consume pmoves-n8n as canonical source --- pmoves/Makefile | 23 +- pmoves/compose/docker-compose.core.yml | 5 +- pmoves/docker-compose.n8n.yml | 4 +- pmoves/n8n/README.md | 13 + pmoves/n8n/flows/discord_voice_agent.json | 23 +- .../n8n/flows/github_runner_autoscaler.json | 168 ++++- .../n8n/flows/github_webhook_processor.json | 152 ++++- .../n8n/flows/langextract_orchestrator.json | 4 +- pmoves/n8n/flows/pmoves_audio_analysis.json | 132 +++- pmoves/n8n/flows/pmoves_channel_monitor.json | 136 +++- pmoves/n8n/flows/pmoves_comfy_hub.json | 167 ++++- .../pmoves_deepresearch_orchestrator.json | 122 +++- pmoves/n8n/flows/pmoves_ingestion_hub.json | 131 +++- pmoves/n8n/flows/pmoves_jellyfin_watcher.json | 147 ++++- .../flows/pmoves_notebook_content_feed.json | 160 ++++- pmoves/n8n/flows/pmoves_social_publisher.json | 188 +++++- pmoves/n8n/flows/pmoves_video_analysis.json | 153 ++++- pmoves/n8n/flows/telegram_voice_agent.json | 23 +- pmoves/n8n/flows/voice_platform_router.json | 579 +++++++++--------- pmoves/n8n/flows/voice_shared_functions.json | 29 +- 20 files changed, 1714 insertions(+), 645 deletions(-) create mode 100644 pmoves/n8n/README.md diff --git a/pmoves/Makefile b/pmoves/Makefile index a1c53a5b7c..4b85f77b92 100644 --- a/pmoves/Makefile +++ b/pmoves/Makefile @@ -1413,6 +1413,8 @@ up-legacy-both: ## Ensure v1 hi-rag gateway CPU+GPU are up # always operate on the same stack regardless of cwd/path. PROJECT ?= pmoves export PROJECT +N8N_SUBMODULE_DIR ?= ../PMOVES-n8n +N8N_CANONICAL_FLOWS_DIR ?= $(N8N_SUBMODULE_DIR)/workflows # n8n persistence mode: # - sqlite: simplest local bring-up (default) @@ -2344,8 +2346,12 @@ up-agents-integrations: ## Start agents using your forks (builds from $(INTEGRAT @$(DC) -f docker-compose.agents.integrations.yml --profile agents up -d nats agent-zero archon archon-ui mesh-agent publisher-discord @echo "✔ Agents started from integrations workspace. Workspace: $(INTEGRATIONS_WORKSPACE)" -.PHONY: up-n8n -up-n8n: ensure-env-shared +.PHONY: up-n8n n8n-sync-submodule-flows n8n-import-flows n8n-activate-flows n8n-bootstrap n8n-export-repo-flows +n8n-sync-submodule-flows: ## Mirror canonical PMOVES-n8n workflows into pmoves/n8n/flows + @$(PYTHON) -c "from pathlib import Path; import shutil; src=Path(r'$(N8N_CANONICAL_FLOWS_DIR)'); dst=Path(r'$(CURDIR)/n8n/flows'); dst.mkdir(parents=True, exist_ok=True); [shutil.copy2(path, dst / path.name) for path in sorted(src.glob('*.json'))]" + @echo "✔ Mirrored PMOVES-n8n workflows into pmoves/n8n/flows" + +up-n8n: ensure-env-shared n8n-sync-submodule-flows @if [ "$(N8N_DB)" = "postgres" ]; then \ echo "→ n8n DB mode: postgres"; \ $(N8N_DC) up -d n8n-db n8n n8n-runners; \ @@ -2359,6 +2365,19 @@ down-n8n: ## Stop n8n workflow services @$(N8N_DC) stop n8n n8n-runners n8n-db >/dev/null 2>&1 || true @$(N8N_DC) rm -f n8n n8n-runners n8n-db >/dev/null 2>&1 || true +n8n-import-flows: ## Import canonical PMOVES-n8n workflows into the live n8n instance + @$(PYTHON) "$(N8N_SUBMODULE_DIR)/scripts/import_repo_flows.py" --container pmoves-n8n --workflow-dir "$(N8N_CANONICAL_FLOWS_DIR)" + +n8n-activate-flows: ## Publish canonical PMOVES-n8n workflows (keeps chat-platform voice flows inactive unless VOICE_PLATFORMS=1) + @$(PYTHON) "$(N8N_SUBMODULE_DIR)/scripts/import_repo_flows.py" --container pmoves-n8n --workflow-dir "$(N8N_CANONICAL_FLOWS_DIR)" --activate-only $(if $(VOICE_PLATFORMS),--voice-platforms,) + +n8n-bootstrap: up-n8n n8n-import-flows n8n-activate-flows ## Bring up n8n, import workflows, and publish the default set + @echo "✔ n8n bootstrap complete" + +n8n-export-repo-flows: ## Export live n8n workflows into PMOVES-n8n and refresh the compatibility mirror + @$(PYTHON) "$(N8N_SUBMODULE_DIR)/scripts/export_repo_flows.py" --container pmoves-n8n --workflow-dir "$(N8N_CANONICAL_FLOWS_DIR)" + @$(MAKE) n8n-sync-submodule-flows + .PHONY: up-comfyui comfyui-smoke up-comfyui: ## Start ComfyUI (docker profile; used by n8n pmoves_comfy_gen flow) @$(DC) --profile creator up -d comfyui diff --git a/pmoves/compose/docker-compose.core.yml b/pmoves/compose/docker-compose.core.yml index 1a2908d74c..2fdbcf5cdc 100644 --- a/pmoves/compose/docker-compose.core.yml +++ b/pmoves/compose/docker-compose.core.yml @@ -3,7 +3,7 @@ version: "3.9" services: n8n: build: - context: .. + context: ../../PMOVES-n8n dockerfile: compose/n8n/Dockerfile image: pmoves/n8n:1.115.3-sqlite restart: unless-stopped @@ -42,8 +42,9 @@ services: - "host.docker.internal:host-gateway" volumes: - n8n-data:/home/node/.n8n + - ../../PMOVES-n8n/workflows:/flows:ro healthcheck: - test: ["CMD", "node", "-e", "fetch('http://localhost:5678/rest/healthz').then(r=>r.ok?process.exit(0):process.exit(1)).catch(()=>process.exit(1))"] + test: ["CMD", "node", "-e", "fetch('http://localhost:5678/healthz').then(r=>r.ok?process.exit(0):process.exit(1)).catch(()=>process.exit(1))"] interval: 10s timeout: 5s retries: 12 diff --git a/pmoves/docker-compose.n8n.yml b/pmoves/docker-compose.n8n.yml index 15904ac361..fc46fcc65b 100644 --- a/pmoves/docker-compose.n8n.yml +++ b/pmoves/docker-compose.n8n.yml @@ -7,7 +7,7 @@ x-env-tier-worker: &env-tier-worker services: n8n: build: - context: . + context: ../PMOVES-n8n dockerfile: compose/n8n/Dockerfile image: pmoves/n8n:2.1.0-sqlite container_name: pmoves-n8n @@ -49,7 +49,7 @@ services: - "host.docker.internal:host-gateway" volumes: - n8n-data:/home/node/.n8n - - ./n8n/flows:/flows:ro + - ../PMOVES-n8n/workflows:/flows:ro n8n-runners: image: n8nio/runners:2.1.0 container_name: pmoves-n8n-runners diff --git a/pmoves/n8n/README.md b/pmoves/n8n/README.md new file mode 100644 index 0000000000..f0584d6efd --- /dev/null +++ b/pmoves/n8n/README.md @@ -0,0 +1,13 @@ +# PMOVES n8n Mirror + +`pmoves/n8n/flows/` is now a compatibility mirror. + +Canonical n8n workflow ownership lives in [`PMOVES-n8n/workflows/`](../../PMOVES-n8n/workflows). The parent repo keeps this mirror so older docs, UI links, and operator muscle memory do not break during the transition. + +Canonical edit path: + +```bash +make -C pmoves n8n-sync-submodule-flows +make -C pmoves n8n-import-flows +make -C pmoves n8n-activate-flows +``` diff --git a/pmoves/n8n/flows/discord_voice_agent.json b/pmoves/n8n/flows/discord_voice_agent.json index 1bacc4b3e7..3cafbef84f 100644 --- a/pmoves/n8n/flows/discord_voice_agent.json +++ b/pmoves/n8n/flows/discord_voice_agent.json @@ -23,10 +23,10 @@ } }, { - "parameters": { - "mode": "rules", - "rules": { - "values": [ + "parameters": { + "mode": "rules", + "rules": { + "values": [ { "conditions": { "options": { @@ -417,19 +417,6 @@ "versionId": "e4e60c53-cc9f-4903-9f62-d8c9e3f65e09", "versionCounter": 4, "triggerCount": 0, - "tags": [ - { - "updatedAt": "2025-12-12T13:18:03.530Z", - "createdAt": "2025-12-12T13:18:03.530Z", - "id": "0A3B8qR44ylHUdFz", - "name": "voice-agent" - }, - { - "updatedAt": "2025-12-12T13:18:05.345Z", - "createdAt": "2025-12-12T13:18:05.345Z", - "id": "LR1zVxH2cj8fd7cd", - "name": "discord" - } - ] + "tags": [] } ] diff --git a/pmoves/n8n/flows/github_runner_autoscaler.json b/pmoves/n8n/flows/github_runner_autoscaler.json index 86c4870850..b604dc8c30 100644 --- a/pmoves/n8n/flows/github_runner_autoscaler.json +++ b/pmoves/n8n/flows/github_runner_autoscaler.json @@ -17,7 +17,10 @@ "name": "Schedule Trigger", "type": "n8n-nodes-base.scheduleTrigger", "typeVersion": 1.1, - "position": [480, 240] + "position": [ + 480, + 240 + ] }, { "parameters": { @@ -29,7 +32,10 @@ "name": "Get Runner Status", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.1, - "position": [700, 240] + "position": [ + 700, + 240 + ] }, { "parameters": { @@ -39,7 +45,10 @@ "name": "Analyze Runner Status", "type": "n8n-nodes-base.functionItem", "typeVersion": 1, - "position": [920, 240] + "position": [ + 920, + 240 + ] }, { "parameters": { @@ -62,7 +71,10 @@ "name": "Check Scale Needed", "type": "n8n-nodes-base.if", "typeVersion": 1, - "position": [1140, 240] + "position": [ + 1140, + 240 + ] }, { "parameters": { @@ -72,7 +84,10 @@ "name": "Check Cloud Capacity", "type": "n8n-nodes-base.functionItem", "typeVersion": 1, - "position": [1380, 120] + "position": [ + 1380, + 120 + ] }, { "parameters": { @@ -89,7 +104,10 @@ "name": "Can Scale Up?", "type": "n8n-nodes-base.if", "typeVersion": 1, - "position": [1600, 120] + "position": [ + 1600, + 120 + ] }, { "parameters": { @@ -99,7 +117,10 @@ "name": "Prepare Scale Alert", "type": "n8n-nodes-base.functionItem", "typeVersion": 1, - "position": [1820, 0] + "position": [ + 1820, + 0 + ] }, { "parameters": { @@ -115,7 +136,10 @@ "name": "Send Discord Alert", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.1, - "position": [2040, 0] + "position": [ + 2040, + 0 + ] }, { "parameters": { @@ -125,7 +149,10 @@ "name": "Prepare Normal Status", "type": "n8n-nodes-base.functionItem", "typeVersion": 1, - "position": [1380, 380] + "position": [ + 1380, + 380 + ] }, { "parameters": { @@ -141,7 +168,10 @@ "name": "Send Status Update", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.1, - "position": [1600, 380] + "position": [ + 1600, + 380 + ] }, { "parameters": { @@ -151,7 +181,10 @@ "name": "Prepare Blocked Alert", "type": "n8n-nodes-base.functionItem", "typeVersion": 1, - "position": [1820, 240] + "position": [ + 1820, + 240 + ] }, { "parameters": { @@ -167,42 +200,125 @@ "name": "Send Blocked Alert", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.1, - "position": [2040, 240] + "position": [ + 2040, + 240 + ] } ], "connections": { "Schedule Trigger": { - "main": [[{"node": "Get Runner Status", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Get Runner Status", + "type": "main", + "index": 0 + } + ] + ] }, "Get Runner Status": { - "main": [[{"node": "Analyze Runner Status", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Analyze Runner Status", + "type": "main", + "index": 0 + } + ] + ] }, "Analyze Runner Status": { - "main": [[{"node": "Check Scale Needed", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Check Scale Needed", + "type": "main", + "index": 0 + } + ] + ] }, "Check Scale Needed": { "main": [ - [{"node": "Check Cloud Capacity", "type": "main", "index": 0}], - [{"node": "Prepare Normal Status", "type": "main", "index": 0}] + [ + { + "node": "Check Cloud Capacity", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Prepare Normal Status", + "type": "main", + "index": 0 + } + ] ] }, "Check Cloud Capacity": { - "main": [[{"node": "Can Scale Up?", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Can Scale Up?", + "type": "main", + "index": 0 + } + ] + ] }, "Can Scale Up?": { "main": [ - [{"node": "Prepare Scale Alert", "type": "main", "index": 0}], - [{"node": "Prepare Blocked Alert", "type": "main", "index": 0}] + [ + { + "node": "Prepare Scale Alert", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Prepare Blocked Alert", + "type": "main", + "index": 0 + } + ] ] }, "Prepare Scale Alert": { - "main": [[{"node": "Send Discord Alert", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Send Discord Alert", + "type": "main", + "index": 0 + } + ] + ] }, "Prepare Normal Status": { - "main": [[{"node": "Send Status Update", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Send Status Update", + "type": "main", + "index": 0 + } + ] + ] }, "Prepare Blocked Alert": { - "main": [[{"node": "Send Blocked Alert", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Send Blocked Alert", + "type": "main", + "index": 0 + } + ] + ] } }, "settings": { @@ -213,6 +329,10 @@ "meta": { "instanceId": "pmoves-github" }, - "tags": ["github", "runners", "autoscaling", "ci-cd"] + "pinData": null, + "versionId": "3446e348-3748-4c23-a713-31d798c7cc95", + "versionCounter": 1, + "triggerCount": 0, + "tags": [] } ] diff --git a/pmoves/n8n/flows/github_webhook_processor.json b/pmoves/n8n/flows/github_webhook_processor.json index 782923a42c..c88250fbc2 100644 --- a/pmoves/n8n/flows/github_webhook_processor.json +++ b/pmoves/n8n/flows/github_webhook_processor.json @@ -14,7 +14,10 @@ "type": "n8n-nodes-base.webhook", "typeVersion": 2, "webhookId": "github-runner-webhook", - "position": [440, 280] + "position": [ + 440, + 280 + ] }, { "parameters": { @@ -24,7 +27,10 @@ "name": "Parse Webhook Headers", "type": "n8n-nodes-base.functionItem", "typeVersion": 1, - "position": [660, 280] + "position": [ + 660, + 280 + ] }, { "parameters": { @@ -53,7 +59,10 @@ "name": "Check Event Type", "type": "n8n-nodes-base.if", "typeVersion": 1, - "position": [880, 280] + "position": [ + 880, + 280 + ] }, { "parameters": { @@ -63,7 +72,10 @@ "name": "Create NATS Event Envelope", "type": "n8n-nodes-base.functionItem", "typeVersion": 1, - "position": [1100, 120] + "position": [ + 1100, + 120 + ] }, { "parameters": { @@ -75,8 +87,14 @@ "options": { "headerParametersUi": { "parameter": [ - {"name": "subject", "value": "={{ $json.nats_subject }}"}, - {"name": "content-type", "value": "application/json"} + { + "name": "subject", + "value": "={{ $json.nats_subject }}" + }, + { + "name": "content-type", + "value": "application/json" + } ] } } @@ -85,7 +103,10 @@ "name": "Publish to NATS", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.1, - "position": [1320, 120] + "position": [ + 1320, + 120 + ] }, { "parameters": { @@ -95,7 +116,10 @@ "name": "Prepare Discord Notification", "type": "n8n-nodes-base.functionItem", "typeVersion": 1, - "position": [1540, 120] + "position": [ + 1540, + 120 + ] }, { "parameters": { @@ -111,7 +135,10 @@ "name": "Send Discord Notification", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.1, - "position": [1760, 120] + "position": [ + 1760, + 120 + ] }, { "parameters": { @@ -122,7 +149,10 @@ "name": "Respond Success", "type": "n8n-nodes-base.respondToWebhook", "typeVersion": 1, - "position": [1980, 120] + "position": [ + 1980, + 120 + ] }, { "parameters": { @@ -132,7 +162,10 @@ "name": "Log Ignored Event", "type": "n8n-nodes-base.functionItem", "typeVersion": 1, - "position": [1100, 380] + "position": [ + 1100, + 380 + ] }, { "parameters": { @@ -143,36 +176,107 @@ "name": "Respond Ignored", "type": "n8n-nodes-base.respondToWebhook", "typeVersion": 1, - "position": [1320, 380] + "position": [ + 1320, + 380 + ] } ], "connections": { "Webhook: GitHub Runner Events": { - "main": [[{"node": "Parse Webhook Headers", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Parse Webhook Headers", + "type": "main", + "index": 0 + } + ] + ] }, "Parse Webhook Headers": { - "main": [[{"node": "Check Event Type", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Check Event Type", + "type": "main", + "index": 0 + } + ] + ] }, "Check Event Type": { "main": [ - [{"node": "Create NATS Event Envelope", "type": "main", "index": 0}], - [{"node": "Log Ignored Event", "type": "main", "index": 0}] + [ + { + "node": "Create NATS Event Envelope", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Log Ignored Event", + "type": "main", + "index": 0 + } + ] ] }, "Create NATS Event Envelope": { - "main": [[{"node": "Publish to NATS", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Publish to NATS", + "type": "main", + "index": 0 + } + ] + ] }, "Publish to NATS": { - "main": [[{"node": "Prepare Discord Notification", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Prepare Discord Notification", + "type": "main", + "index": 0 + } + ] + ] }, "Prepare Discord Notification": { - "main": [[{"node": "Send Discord Notification", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Send Discord Notification", + "type": "main", + "index": 0 + } + ] + ] }, "Send Discord Notification": { - "main": [[{"node": "Respond Success", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Respond Success", + "type": "main", + "index": 0 + } + ] + ] }, "Log Ignored Event": { - "main": [[{"node": "Respond Ignored", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Respond Ignored", + "type": "main", + "index": 0 + } + ] + ] } }, "settings": { @@ -183,6 +287,10 @@ "meta": { "instanceId": "pmoves-github" }, - "tags": ["github", "webhooks", "nats", "ci-cd"] + "pinData": null, + "versionId": "58a9f471-9d35-4f01-beb6-2049c48bbfe5", + "versionCounter": 1, + "triggerCount": 0, + "tags": [] } ] diff --git a/pmoves/n8n/flows/langextract_orchestrator.json b/pmoves/n8n/flows/langextract_orchestrator.json index ace5614ddf..8dd93ff7c0 100644 --- a/pmoves/n8n/flows/langextract_orchestrator.json +++ b/pmoves/n8n/flows/langextract_orchestrator.json @@ -252,9 +252,9 @@ "description": "Process documents via LangExtract orchestrator pipeline. Accepts file paths and returns extraction results with chunk counts and ingestion status." }, "pinData": null, - "versionId": "langextract-orch-v1", + "versionId": "c904e951-20ec-4034-acd6-9e2833c4eac9", "versionCounter": 1, "triggerCount": 0, - "tags": ["langextract", "orchestrator", "document-processing"] + "tags": [] } ] diff --git a/pmoves/n8n/flows/pmoves_audio_analysis.json b/pmoves/n8n/flows/pmoves_audio_analysis.json index 6941cf3f45..fa822c1429 100644 --- a/pmoves/n8n/flows/pmoves_audio_analysis.json +++ b/pmoves/n8n/flows/pmoves_audio_analysis.json @@ -14,7 +14,10 @@ "name": "Webhook Trigger", "type": "n8n-nodes-base.webhook", "typeVersion": 1, - "position": [200, 300] + "position": [ + 200, + 300 + ] }, { "parameters": { @@ -24,7 +27,10 @@ "name": "Prepare Audio Request", "type": "n8n-nodes-base.function", "typeVersion": 2, - "position": [460, 300] + "position": [ + 460, + 300 + ] }, { "parameters": { @@ -41,7 +47,10 @@ "name": "FFmpeg-Whisper Transcribe", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4, - "position": [720, 200] + "position": [ + 720, + 200 + ] }, { "parameters": { @@ -58,7 +67,10 @@ "name": "Media-Audio Analysis", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4, - "position": [720, 400] + "position": [ + 720, + 400 + ] }, { "parameters": { @@ -68,7 +80,10 @@ "name": "Merge Results", "type": "n8n-nodes-base.function", "typeVersion": 2, - "position": [980, 300] + "position": [ + 980, + 300 + ] }, { "parameters": { @@ -78,7 +93,10 @@ "name": "Build CGP Envelope", "type": "n8n-nodes-base.function", "typeVersion": 2, - "position": [1240, 300] + "position": [ + 1240, + 300 + ] }, { "parameters": { @@ -95,7 +113,10 @@ "name": "Index to Hi-RAG", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4, - "position": [1500, 300] + "position": [ + 1500, + 300 + ] }, { "parameters": { @@ -112,7 +133,10 @@ "name": "Publish NATS Event", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4, - "position": [1500, 500] + "position": [ + 1500, + 500 + ] }, { "parameters": { @@ -129,7 +153,10 @@ "name": "Auto Research?", "type": "n8n-nodes-base.if", "typeVersion": 1, - "position": [1760, 300] + "position": [ + 1760, + 300 + ] }, { "parameters": { @@ -146,42 +173,103 @@ "name": "Trigger DeepResearch", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4, - "position": [2020, 200] + "position": [ + 2020, + 200 + ] } ], "connections": { "Webhook Trigger": { - "main": [[{"node": "Prepare Audio Request", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Prepare Audio Request", + "type": "main", + "index": 0 + } + ] + ] }, "Prepare Audio Request": { "main": [ [ - {"node": "FFmpeg-Whisper Transcribe", "type": "main", "index": 0}, - {"node": "Media-Audio Analysis", "type": "main", "index": 0} + { + "node": "FFmpeg-Whisper Transcribe", + "type": "main", + "index": 0 + }, + { + "node": "Media-Audio Analysis", + "type": "main", + "index": 0 + } ] ] }, "FFmpeg-Whisper Transcribe": { - "main": [[{"node": "Merge Results", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Merge Results", + "type": "main", + "index": 0 + } + ] + ] }, "Media-Audio Analysis": { - "main": [[{"node": "Merge Results", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Merge Results", + "type": "main", + "index": 0 + } + ] + ] }, "Merge Results": { - "main": [[{"node": "Build CGP Envelope", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Build CGP Envelope", + "type": "main", + "index": 0 + } + ] + ] }, "Build CGP Envelope": { "main": [ [ - {"node": "Index to Hi-RAG", "type": "main", "index": 0}, - {"node": "Publish NATS Event", "type": "main", "index": 0}, - {"node": "Auto Research?", "type": "main", "index": 0} + { + "node": "Index to Hi-RAG", + "type": "main", + "index": 0 + }, + { + "node": "Publish NATS Event", + "type": "main", + "index": 0 + }, + { + "node": "Auto Research?", + "type": "main", + "index": 0 + } ] ] }, "Auto Research?": { "main": [ - [{"node": "Trigger DeepResearch", "type": "main", "index": 0}], + [ + { + "node": "Trigger DeepResearch", + "type": "main", + "index": 0 + } + ], [] ] } @@ -193,9 +281,9 @@ "description": "Audio analysis pipeline: transcription (FFmpeg-Whisper) + emotion/feature analysis (Media-Audio) + Hi-RAG indexing" }, "pinData": null, - "versionId": "audio-analysis-v1", + "versionId": "4e367856-aed6-45ba-abc6-278685ac3bbb", "versionCounter": 1, "triggerCount": 0, - "tags": ["pmoves", "audio", "analysis", "transcription"] + "tags": [] } ] diff --git a/pmoves/n8n/flows/pmoves_channel_monitor.json b/pmoves/n8n/flows/pmoves_channel_monitor.json index b5b6f6ed02..2fd49ea1b3 100644 --- a/pmoves/n8n/flows/pmoves_channel_monitor.json +++ b/pmoves/n8n/flows/pmoves_channel_monitor.json @@ -14,7 +14,10 @@ "name": "Channel Monitor Webhook", "type": "n8n-nodes-base.webhook", "typeVersion": 1, - "position": [200, 300] + "position": [ + 200, + 300 + ] }, { "parameters": { @@ -24,7 +27,10 @@ "name": "Parse Channel Notification", "type": "n8n-nodes-base.function", "typeVersion": 2, - "position": [460, 300] + "position": [ + 460, + 300 + ] }, { "parameters": { @@ -41,7 +47,10 @@ "name": "Publish Channel Event", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4, - "position": [720, 200] + "position": [ + 720, + 200 + ] }, { "parameters": { @@ -58,7 +67,10 @@ "name": "Auto Ingest?", "type": "n8n-nodes-base.if", "typeVersion": 1, - "position": [720, 400] + "position": [ + 720, + 400 + ] }, { "parameters": { @@ -75,7 +87,10 @@ "name": "Trigger PMOVES.YT Ingest", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4, - "position": [980, 300] + "position": [ + 980, + 300 + ] }, { "parameters": { @@ -85,7 +100,10 @@ "name": "Track Ingest Job", "type": "n8n-nodes-base.function", "typeVersion": 2, - "position": [1240, 300] + "position": [ + 1240, + 300 + ] }, { "parameters": { @@ -95,7 +113,10 @@ "name": "Build Social Preview", "type": "n8n-nodes-base.function", "typeVersion": 2, - "position": [1500, 300] + "position": [ + 1500, + 300 + ] }, { "parameters": { @@ -112,7 +133,10 @@ "name": "Post Preview to Discord", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4, - "position": [1760, 200] + "position": [ + 1760, + 200 + ] }, { "parameters": { @@ -129,7 +153,10 @@ "name": "Auto Research?", "type": "n8n-nodes-base.if", "typeVersion": 1, - "position": [1760, 400] + "position": [ + 1760, + 400 + ] }, { "parameters": { @@ -146,40 +173,99 @@ "name": "Trigger DeepResearch", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4, - "position": [2020, 400] + "position": [ + 2020, + 400 + ] } ], "connections": { "Channel Monitor Webhook": { - "main": [[{"node": "Parse Channel Notification", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Parse Channel Notification", + "type": "main", + "index": 0 + } + ] + ] }, "Parse Channel Notification": { - "main": [[ - {"node": "Publish Channel Event", "type": "main", "index": 0}, - {"node": "Auto Ingest?", "type": "main", "index": 0} - ]] + "main": [ + [ + { + "node": "Publish Channel Event", + "type": "main", + "index": 0 + }, + { + "node": "Auto Ingest?", + "type": "main", + "index": 0 + } + ] + ] }, "Auto Ingest?": { "main": [ - [{"node": "Trigger PMOVES.YT Ingest", "type": "main", "index": 0}], + [ + { + "node": "Trigger PMOVES.YT Ingest", + "type": "main", + "index": 0 + } + ], [] ] }, "Trigger PMOVES.YT Ingest": { - "main": [[{"node": "Track Ingest Job", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Track Ingest Job", + "type": "main", + "index": 0 + } + ] + ] }, "Track Ingest Job": { - "main": [[{"node": "Build Social Preview", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Build Social Preview", + "type": "main", + "index": 0 + } + ] + ] }, "Build Social Preview": { - "main": [[ - {"node": "Post Preview to Discord", "type": "main", "index": 0}, - {"node": "Auto Research?", "type": "main", "index": 0} - ]] + "main": [ + [ + { + "node": "Post Preview to Discord", + "type": "main", + "index": 0 + }, + { + "node": "Auto Research?", + "type": "main", + "index": 0 + } + ] + ] }, "Auto Research?": { "main": [ - [{"node": "Trigger DeepResearch", "type": "main", "index": 0}], + [ + { + "node": "Trigger DeepResearch", + "type": "main", + "index": 0 + } + ], [] ] } @@ -191,9 +277,9 @@ "description": "YouTube Channel Monitor integration: receives new content alerts, triggers PMOVES.YT ingestion, posts social preview" }, "pinData": null, - "versionId": "channel-monitor-v1", + "versionId": "1aa17c99-8ea9-48d7-96ea-9e2094d45a73", "versionCounter": 1, "triggerCount": 0, - "tags": ["pmoves", "youtube", "channel-monitor", "ingestion"] + "tags": [] } ] diff --git a/pmoves/n8n/flows/pmoves_comfy_hub.json b/pmoves/n8n/flows/pmoves_comfy_hub.json index 66d47fc941..fff4dff128 100644 --- a/pmoves/n8n/flows/pmoves_comfy_hub.json +++ b/pmoves/n8n/flows/pmoves_comfy_hub.json @@ -14,7 +14,10 @@ "name": "Webhook Trigger", "type": "n8n-nodes-base.webhook", "typeVersion": 1, - "position": [200, 300] + "position": [ + 200, + 300 + ] }, { "parameters": { @@ -24,7 +27,10 @@ "name": "Prepare Generation Job", "type": "n8n-nodes-base.function", "typeVersion": 2, - "position": [460, 300] + "position": [ + 460, + 300 + ] }, { "parameters": { @@ -41,7 +47,10 @@ "name": "Is Qwen Edit?", "type": "n8n-nodes-base.if", "typeVersion": 1, - "position": [720, 100] + "position": [ + 720, + 100 + ] }, { "parameters": { @@ -58,7 +67,10 @@ "name": "Is WAN Animate?", "type": "n8n-nodes-base.if", "typeVersion": 1, - "position": [720, 300] + "position": [ + 720, + 300 + ] }, { "parameters": { @@ -76,7 +88,10 @@ "name": "Is Voice?", "type": "n8n-nodes-base.if", "typeVersion": 1, - "position": [720, 500] + "position": [ + 720, + 500 + ] }, { "parameters": { @@ -86,7 +101,10 @@ "name": "Build Qwen Prompt", "type": "n8n-nodes-base.function", "typeVersion": 2, - "position": [980, 100] + "position": [ + 980, + 100 + ] }, { "parameters": { @@ -96,7 +114,10 @@ "name": "Build WAN Prompt", "type": "n8n-nodes-base.function", "typeVersion": 2, - "position": [980, 300] + "position": [ + 980, + 300 + ] }, { "parameters": { @@ -106,7 +127,10 @@ "name": "Build Voice Prompt", "type": "n8n-nodes-base.function", "typeVersion": 2, - "position": [980, 500] + "position": [ + 980, + 500 + ] }, { "parameters": { @@ -123,7 +147,10 @@ "name": "Submit to ComfyUI", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4, - "position": [1240, 300] + "position": [ + 1240, + 300 + ] }, { "parameters": { @@ -133,7 +160,10 @@ "name": "Track Job Status", "type": "n8n-nodes-base.function", "typeVersion": 2, - "position": [1500, 300] + "position": [ + 1500, + 300 + ] }, { "parameters": { @@ -150,52 +180,135 @@ "name": "Publish Generation Event", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4, - "position": [1760, 300] + "position": [ + 1760, + 300 + ] } ], "connections": { "Webhook Trigger": { - "main": [[{"node": "Prepare Generation Job", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Prepare Generation Job", + "type": "main", + "index": 0 + } + ] + ] }, "Prepare Generation Job": { - "main": [[ - {"node": "Is Qwen Edit?", "type": "main", "index": 0}, - {"node": "Is WAN Animate?", "type": "main", "index": 0}, - {"node": "Is Voice?", "type": "main", "index": 0} - ]] + "main": [ + [ + { + "node": "Is Qwen Edit?", + "type": "main", + "index": 0 + }, + { + "node": "Is WAN Animate?", + "type": "main", + "index": 0 + }, + { + "node": "Is Voice?", + "type": "main", + "index": 0 + } + ] + ] }, "Is Qwen Edit?": { "main": [ - [{"node": "Build Qwen Prompt", "type": "main", "index": 0}], + [ + { + "node": "Build Qwen Prompt", + "type": "main", + "index": 0 + } + ], [] ] }, "Is WAN Animate?": { "main": [ - [{"node": "Build WAN Prompt", "type": "main", "index": 0}], + [ + { + "node": "Build WAN Prompt", + "type": "main", + "index": 0 + } + ], [] ] }, "Is Voice?": { "main": [ - [{"node": "Build Voice Prompt", "type": "main", "index": 0}], + [ + { + "node": "Build Voice Prompt", + "type": "main", + "index": 0 + } + ], [] ] }, "Build Qwen Prompt": { - "main": [[{"node": "Submit to ComfyUI", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Submit to ComfyUI", + "type": "main", + "index": 0 + } + ] + ] }, "Build WAN Prompt": { - "main": [[{"node": "Submit to ComfyUI", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Submit to ComfyUI", + "type": "main", + "index": 0 + } + ] + ] }, "Build Voice Prompt": { - "main": [[{"node": "Submit to ComfyUI", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Submit to ComfyUI", + "type": "main", + "index": 0 + } + ] + ] }, "Submit to ComfyUI": { - "main": [[{"node": "Track Job Status", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Track Job Status", + "type": "main", + "index": 0 + } + ] + ] }, "Track Job Status": { - "main": [[{"node": "Publish Generation Event", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Publish Generation Event", + "type": "main", + "index": 0 + } + ] + ] } }, "settings": {}, @@ -205,9 +318,9 @@ "description": "ComfyUI generation hub: routes generation requests to appropriate workflow (Qwen Image Edit, WAN Animate, VibeVoice)" }, "pinData": null, - "versionId": "comfy-hub-v1", + "versionId": "2fe9ede5-8936-4fbb-a4d2-d9cb291a9729", "versionCounter": 1, "triggerCount": 0, - "tags": ["pmoves", "comfyui", "generation", "creative"] + "tags": [] } ] diff --git a/pmoves/n8n/flows/pmoves_deepresearch_orchestrator.json b/pmoves/n8n/flows/pmoves_deepresearch_orchestrator.json index 1beca2f315..60541bf241 100644 --- a/pmoves/n8n/flows/pmoves_deepresearch_orchestrator.json +++ b/pmoves/n8n/flows/pmoves_deepresearch_orchestrator.json @@ -14,7 +14,10 @@ "name": "Webhook Trigger", "type": "n8n-nodes-base.webhook", "typeVersion": 1, - "position": [200, 300] + "position": [ + 200, + 300 + ] }, { "parameters": { @@ -24,7 +27,10 @@ "name": "Prepare Research Request", "type": "n8n-nodes-base.function", "typeVersion": 2, - "position": [460, 300] + "position": [ + 460, + 300 + ] }, { "parameters": { @@ -41,7 +47,10 @@ "name": "Publish Research Request", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4, - "position": [720, 300] + "position": [ + 720, + 300 + ] }, { "parameters": { @@ -58,7 +67,10 @@ "name": "Call DeepResearch API", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4, - "position": [720, 500] + "position": [ + 720, + 500 + ] }, { "parameters": { @@ -68,7 +80,10 @@ "name": "Process Research Result", "type": "n8n-nodes-base.function", "typeVersion": 2, - "position": [980, 500] + "position": [ + 980, + 500 + ] }, { "parameters": { @@ -78,7 +93,10 @@ "name": "Build CGP Envelope", "type": "n8n-nodes-base.function", "typeVersion": 2, - "position": [1240, 500] + "position": [ + 1240, + 500 + ] }, { "parameters": { @@ -95,7 +113,10 @@ "name": "Index to Hi-RAG", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4, - "position": [1500, 400] + "position": [ + 1500, + 400 + ] }, { "parameters": { @@ -112,7 +133,10 @@ "name": "Publish Result Event", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4, - "position": [1500, 600] + "position": [ + 1500, + 600 + ] }, { "parameters": { @@ -129,7 +153,10 @@ "name": "Auto Publish?", "type": "n8n-nodes-base.if", "typeVersion": 1, - "position": [1760, 500] + "position": [ + 1760, + 500 + ] }, { "parameters": { @@ -146,39 +173,92 @@ "name": "Trigger Content Generation", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4, - "position": [2020, 400] + "position": [ + 2020, + 400 + ] } ], "connections": { "Webhook Trigger": { - "main": [[{"node": "Prepare Research Request", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Prepare Research Request", + "type": "main", + "index": 0 + } + ] + ] }, "Prepare Research Request": { "main": [ [ - {"node": "Publish Research Request", "type": "main", "index": 0}, - {"node": "Call DeepResearch API", "type": "main", "index": 0} + { + "node": "Publish Research Request", + "type": "main", + "index": 0 + }, + { + "node": "Call DeepResearch API", + "type": "main", + "index": 0 + } ] ] }, "Call DeepResearch API": { - "main": [[{"node": "Process Research Result", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Process Research Result", + "type": "main", + "index": 0 + } + ] + ] }, "Process Research Result": { - "main": [[{"node": "Build CGP Envelope", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Build CGP Envelope", + "type": "main", + "index": 0 + } + ] + ] }, "Build CGP Envelope": { "main": [ [ - {"node": "Index to Hi-RAG", "type": "main", "index": 0}, - {"node": "Publish Result Event", "type": "main", "index": 0}, - {"node": "Auto Publish?", "type": "main", "index": 0} + { + "node": "Index to Hi-RAG", + "type": "main", + "index": 0 + }, + { + "node": "Publish Result Event", + "type": "main", + "index": 0 + }, + { + "node": "Auto Publish?", + "type": "main", + "index": 0 + } ] ] }, "Auto Publish?": { "main": [ - [{"node": "Trigger Content Generation", "type": "main", "index": 0}], + [ + { + "node": "Trigger Content Generation", + "type": "main", + "index": 0 + } + ], [] ] } @@ -190,9 +270,9 @@ "description": "DeepResearch orchestrator: triggers research, indexes results to Hi-RAG, publishes NATS events, optionally triggers content generation" }, "pinData": null, - "versionId": "deepresearch-v1", + "versionId": "8ab8e5c9-32d7-438a-96bb-cf47ffa24302", "versionCounter": 1, "triggerCount": 0, - "tags": ["pmoves", "research", "deepresearch", "knowledge"] + "tags": [] } ] diff --git a/pmoves/n8n/flows/pmoves_ingestion_hub.json b/pmoves/n8n/flows/pmoves_ingestion_hub.json index 809fb8fea1..3e752a0da7 100644 --- a/pmoves/n8n/flows/pmoves_ingestion_hub.json +++ b/pmoves/n8n/flows/pmoves_ingestion_hub.json @@ -14,7 +14,10 @@ "name": "Webhook Trigger", "type": "n8n-nodes-base.webhook", "typeVersion": 1, - "position": [200, 300] + "position": [ + 200, + 300 + ] }, { "parameters": { @@ -24,7 +27,10 @@ "name": "Detect Content Type", "type": "n8n-nodes-base.function", "typeVersion": 2, - "position": [460, 300] + "position": [ + 460, + 300 + ] }, { "parameters": { @@ -41,7 +47,10 @@ "name": "Is Audio?", "type": "n8n-nodes-base.if", "typeVersion": 1, - "position": [720, 100] + "position": [ + 720, + 100 + ] }, { "parameters": { @@ -58,7 +67,10 @@ "name": "Is Video?", "type": "n8n-nodes-base.if", "typeVersion": 1, - "position": [720, 300] + "position": [ + 720, + 300 + ] }, { "parameters": { @@ -75,7 +87,10 @@ "name": "Is Image?", "type": "n8n-nodes-base.if", "typeVersion": 1, - "position": [720, 500] + "position": [ + 720, + 500 + ] }, { "parameters": { @@ -92,7 +107,10 @@ "name": "Trigger Audio Pipeline", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4, - "position": [980, 100] + "position": [ + 980, + 100 + ] }, { "parameters": { @@ -109,7 +127,10 @@ "name": "Trigger Video Pipeline", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4, - "position": [980, 300] + "position": [ + 980, + 300 + ] }, { "parameters": { @@ -126,7 +147,10 @@ "name": "Trigger Image Pipeline", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4, - "position": [980, 500] + "position": [ + 980, + 500 + ] }, { "parameters": { @@ -136,36 +160,97 @@ "name": "Log Unhandled", "type": "n8n-nodes-base.function", "typeVersion": 2, - "position": [980, 700] + "position": [ + 980, + 700 + ] } ], "connections": { "Webhook Trigger": { - "main": [[{"node": "Detect Content Type", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Detect Content Type", + "type": "main", + "index": 0 + } + ] + ] }, "Detect Content Type": { - "main": [[ - {"node": "Is Audio?", "type": "main", "index": 0}, - {"node": "Is Video?", "type": "main", "index": 0}, - {"node": "Is Image?", "type": "main", "index": 0} - ]] + "main": [ + [ + { + "node": "Is Audio?", + "type": "main", + "index": 0 + }, + { + "node": "Is Video?", + "type": "main", + "index": 0 + }, + { + "node": "Is Image?", + "type": "main", + "index": 0 + } + ] + ] }, "Is Audio?": { "main": [ - [{"node": "Trigger Audio Pipeline", "type": "main", "index": 0}], - [{"node": "Is Video?", "type": "main", "index": 0}] + [ + { + "node": "Trigger Audio Pipeline", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Is Video?", + "type": "main", + "index": 0 + } + ] ] }, "Is Video?": { "main": [ - [{"node": "Trigger Video Pipeline", "type": "main", "index": 0}], - [{"node": "Is Image?", "type": "main", "index": 0}] + [ + { + "node": "Trigger Video Pipeline", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Is Image?", + "type": "main", + "index": 0 + } + ] ] }, "Is Image?": { "main": [ - [{"node": "Trigger Image Pipeline", "type": "main", "index": 0}], - [{"node": "Log Unhandled", "type": "main", "index": 0}] + [ + { + "node": "Trigger Image Pipeline", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Log Unhandled", + "type": "main", + "index": 0 + } + ] ] } }, @@ -176,9 +261,9 @@ "description": "Central dispatcher for all content ingestion - routes to appropriate analysis pipeline based on content type" }, "pinData": null, - "versionId": "ingestion-hub-v1", + "versionId": "60492d99-f1c2-46f1-9d09-a0fa3af8a383", "versionCounter": 1, "triggerCount": 0, - "tags": ["pmoves", "ingestion", "routing"] + "tags": [] } ] diff --git a/pmoves/n8n/flows/pmoves_jellyfin_watcher.json b/pmoves/n8n/flows/pmoves_jellyfin_watcher.json index 8599b5eeab..df54c21799 100644 --- a/pmoves/n8n/flows/pmoves_jellyfin_watcher.json +++ b/pmoves/n8n/flows/pmoves_jellyfin_watcher.json @@ -14,7 +14,10 @@ "name": "Jellyfin Webhook", "type": "n8n-nodes-base.webhook", "typeVersion": 1, - "position": [200, 300] + "position": [ + 200, + 300 + ] }, { "parameters": { @@ -24,7 +27,10 @@ "name": "Parse Jellyfin Event", "type": "n8n-nodes-base.function", "typeVersion": 2, - "position": [460, 300] + "position": [ + 460, + 300 + ] }, { "parameters": { @@ -42,7 +48,10 @@ "name": "Is Item Added?", "type": "n8n-nodes-base.if", "typeVersion": 1, - "position": [720, 200] + "position": [ + 720, + 200 + ] }, { "parameters": { @@ -60,7 +69,10 @@ "name": "Is Playback Event?", "type": "n8n-nodes-base.if", "typeVersion": 1, - "position": [720, 400] + "position": [ + 720, + 400 + ] }, { "parameters": { @@ -70,7 +82,10 @@ "name": "Build Ingest Request", "type": "n8n-nodes-base.function", "typeVersion": 2, - "position": [980, 200] + "position": [ + 980, + 200 + ] }, { "parameters": { @@ -87,7 +102,10 @@ "name": "Publish Item Added Event", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4, - "position": [1240, 100] + "position": [ + 1240, + 100 + ] }, { "parameters": { @@ -105,7 +123,10 @@ "name": "Is Video Content?", "type": "n8n-nodes-base.if", "typeVersion": 1, - "position": [1240, 300] + "position": [ + 1240, + 300 + ] }, { "parameters": { @@ -122,7 +143,10 @@ "name": "Trigger Video Analysis", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4, - "position": [1500, 200] + "position": [ + 1500, + 200 + ] }, { "parameters": { @@ -139,7 +163,10 @@ "name": "Trigger Audio Analysis", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4, - "position": [1500, 400] + "position": [ + 1500, + 400 + ] }, { "parameters": { @@ -149,7 +176,10 @@ "name": "Log Playback Event", "type": "n8n-nodes-base.function", "typeVersion": 2, - "position": [980, 400] + "position": [ + 980, + 400 + ] }, { "parameters": { @@ -166,45 +196,108 @@ "name": "Publish Playback Event", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4, - "position": [1240, 400] + "position": [ + 1240, + 400 + ] } ], "connections": { "Jellyfin Webhook": { - "main": [[{"node": "Parse Jellyfin Event", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Parse Jellyfin Event", + "type": "main", + "index": 0 + } + ] + ] }, "Parse Jellyfin Event": { - "main": [[ - {"node": "Is Item Added?", "type": "main", "index": 0}, - {"node": "Is Playback Event?", "type": "main", "index": 0} - ]] + "main": [ + [ + { + "node": "Is Item Added?", + "type": "main", + "index": 0 + }, + { + "node": "Is Playback Event?", + "type": "main", + "index": 0 + } + ] + ] }, "Is Item Added?": { "main": [ - [{"node": "Build Ingest Request", "type": "main", "index": 0}], + [ + { + "node": "Build Ingest Request", + "type": "main", + "index": 0 + } + ], [] ] }, "Is Playback Event?": { "main": [ - [{"node": "Log Playback Event", "type": "main", "index": 0}], + [ + { + "node": "Log Playback Event", + "type": "main", + "index": 0 + } + ], [] ] }, "Build Ingest Request": { - "main": [[ - {"node": "Publish Item Added Event", "type": "main", "index": 0}, - {"node": "Is Video Content?", "type": "main", "index": 0} - ]] + "main": [ + [ + { + "node": "Publish Item Added Event", + "type": "main", + "index": 0 + }, + { + "node": "Is Video Content?", + "type": "main", + "index": 0 + } + ] + ] }, "Is Video Content?": { "main": [ - [{"node": "Trigger Video Analysis", "type": "main", "index": 0}], - [{"node": "Trigger Audio Analysis", "type": "main", "index": 0}] + [ + { + "node": "Trigger Video Analysis", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Trigger Audio Analysis", + "type": "main", + "index": 0 + } + ] ] }, "Log Playback Event": { - "main": [[{"node": "Publish Playback Event", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Publish Playback Event", + "type": "main", + "index": 0 + } + ] + ] } }, "settings": {}, @@ -214,9 +307,9 @@ "description": "Jellyfin event watcher: handles item additions and playback events, triggers analysis pipelines" }, "pinData": null, - "versionId": "jellyfin-watcher-v1", + "versionId": "c11fc268-081a-461b-8355-b6c8caa598ba", "versionCounter": 1, "triggerCount": 0, - "tags": ["pmoves", "jellyfin", "media", "ingestion"] + "tags": [] } ] diff --git a/pmoves/n8n/flows/pmoves_notebook_content_feed.json b/pmoves/n8n/flows/pmoves_notebook_content_feed.json index 484f6075e3..2d753af4fe 100644 --- a/pmoves/n8n/flows/pmoves_notebook_content_feed.json +++ b/pmoves/n8n/flows/pmoves_notebook_content_feed.json @@ -14,7 +14,10 @@ "name": "Notebook Sync Webhook", "type": "n8n-nodes-base.webhook", "typeVersion": 1, - "position": [200, 300] + "position": [ + 200, + 300 + ] }, { "parameters": { @@ -24,7 +27,10 @@ "name": "Parse Notebook Content", "type": "n8n-nodes-base.function", "typeVersion": 2, - "position": [460, 300] + "position": [ + 460, + 300 + ] }, { "parameters": { @@ -41,7 +47,10 @@ "name": "Publish Sync Event", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4, - "position": [720, 200] + "position": [ + 720, + 200 + ] }, { "parameters": { @@ -58,7 +67,10 @@ "name": "Is Publishable?", "type": "n8n-nodes-base.if", "typeVersion": 1, - "position": [720, 400] + "position": [ + 720, + 400 + ] }, { "parameters": { @@ -68,7 +80,10 @@ "name": "Generate Social Snippets", "type": "n8n-nodes-base.function", "typeVersion": 2, - "position": [980, 400] + "position": [ + 980, + 400 + ] }, { "parameters": { @@ -78,7 +93,10 @@ "name": "Create Studio Board Draft", "type": "n8n-nodes-base.function", "typeVersion": 2, - "position": [1240, 400] + "position": [ + 1240, + 400 + ] }, { "parameters": { @@ -87,21 +105,38 @@ "sendHeaders": true, "headerParameters": { "parameters": [ - {"name": "content-type", "value": "application/json"}, - {"name": "apikey", "value": "={{ $env.SUPABASE_SERVICE_ROLE_KEY }}"}, - {"name": "Authorization", "value": "=Bearer {{$env.SUPABASE_SERVICE_ROLE_KEY}}"}, - {"name": "Prefer", "value": "return=representation"} + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "apikey", + "value": "={{ $env.SUPABASE_SERVICE_ROLE_KEY }}" + }, + { + "name": "Authorization", + "value": "=Bearer {{$env.SUPABASE_SERVICE_ROLE_KEY}}" + }, + { + "name": "Prefer", + "value": "return=representation" + } ] }, "sendBody": true, "body": "={{ JSON.stringify($json) }}", - "options": {"timeout": 10000} + "options": { + "timeout": 10000 + } }, "id": "save_draft", "name": "Save to Studio Board", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4, - "position": [1500, 400] + "position": [ + 1500, + 400 + ] }, { "parameters": { @@ -111,7 +146,10 @@ "name": "Build CGP Envelope", "type": "n8n-nodes-base.function", "typeVersion": 2, - "position": [720, 600] + "position": [ + 720, + 600 + ] }, { "parameters": { @@ -128,7 +166,10 @@ "name": "Index to Hi-RAG", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4, - "position": [980, 600] + "position": [ + 980, + 600 + ] }, { "parameters": { @@ -145,37 +186,100 @@ "name": "Publish Draft Created Event", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4, - "position": [1760, 400] + "position": [ + 1760, + 400 + ] } ], "connections": { "Notebook Sync Webhook": { - "main": [[{"node": "Parse Notebook Content", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Parse Notebook Content", + "type": "main", + "index": 0 + } + ] + ] }, "Parse Notebook Content": { - "main": [[ - {"node": "Publish Sync Event", "type": "main", "index": 0}, - {"node": "Is Publishable?", "type": "main", "index": 0}, - {"node": "Build CGP Envelope", "type": "main", "index": 0} - ]] + "main": [ + [ + { + "node": "Publish Sync Event", + "type": "main", + "index": 0 + }, + { + "node": "Is Publishable?", + "type": "main", + "index": 0 + }, + { + "node": "Build CGP Envelope", + "type": "main", + "index": 0 + } + ] + ] }, "Is Publishable?": { "main": [ - [{"node": "Generate Social Snippets", "type": "main", "index": 0}], + [ + { + "node": "Generate Social Snippets", + "type": "main", + "index": 0 + } + ], [] ] }, "Generate Social Snippets": { - "main": [[{"node": "Create Studio Board Draft", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Create Studio Board Draft", + "type": "main", + "index": 0 + } + ] + ] }, "Create Studio Board Draft": { - "main": [[{"node": "Save to Studio Board", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Save to Studio Board", + "type": "main", + "index": 0 + } + ] + ] }, "Save to Studio Board": { - "main": [[{"node": "Publish Draft Created Event", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Publish Draft Created Event", + "type": "main", + "index": 0 + } + ] + ] }, "Build CGP Envelope": { - "main": [[{"node": "Index to Hi-RAG", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Index to Hi-RAG", + "type": "main", + "index": 0 + } + ] + ] } }, "settings": {}, @@ -185,9 +289,9 @@ "description": "Open Notebook content feed: syncs notebook content, creates studio_board drafts, indexes to Hi-RAG" }, "pinData": null, - "versionId": "notebook-feed-v1", + "versionId": "ba51090c-fc13-42d1-9e00-a1b2468cec70", "versionCounter": 1, "triggerCount": 0, - "tags": ["pmoves", "notebook", "content", "publishing"] + "tags": [] } ] diff --git a/pmoves/n8n/flows/pmoves_social_publisher.json b/pmoves/n8n/flows/pmoves_social_publisher.json index 28fb418bc7..b159ffe03c 100644 --- a/pmoves/n8n/flows/pmoves_social_publisher.json +++ b/pmoves/n8n/flows/pmoves_social_publisher.json @@ -14,7 +14,10 @@ "name": "Webhook Trigger", "type": "n8n-nodes-base.webhook", "typeVersion": 1, - "position": [200, 300] + "position": [ + 200, + 300 + ] }, { "parameters": { @@ -24,7 +27,10 @@ "name": "Prepare Publish Request", "type": "n8n-nodes-base.function", "typeVersion": 2, - "position": [460, 300] + "position": [ + 460, + 300 + ] }, { "parameters": { @@ -34,7 +40,10 @@ "name": "Build Discord Payload", "type": "n8n-nodes-base.function", "typeVersion": 2, - "position": [720, 200] + "position": [ + 720, + 200 + ] }, { "parameters": { @@ -44,7 +53,10 @@ "name": "Build Twitter Thread", "type": "n8n-nodes-base.function", "typeVersion": 2, - "position": [720, 400] + "position": [ + 720, + 400 + ] }, { "parameters": { @@ -61,7 +73,10 @@ "name": "Post to Discord", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4, - "position": [1000, 200] + "position": [ + 1000, + 200 + ] }, { "parameters": { @@ -79,7 +94,10 @@ "name": "Publish to Twitter?", "type": "n8n-nodes-base.if", "typeVersion": 1, - "position": [1000, 400] + "position": [ + 1000, + 400 + ] }, { "parameters": { @@ -89,7 +107,10 @@ "name": "Post to Twitter", "type": "n8n-nodes-base.function", "typeVersion": 2, - "position": [1260, 400] + "position": [ + 1260, + 400 + ] }, { "parameters": { @@ -99,7 +120,10 @@ "name": "Collect Results", "type": "n8n-nodes-base.function", "typeVersion": 2, - "position": [1520, 300] + "position": [ + 1520, + 300 + ] }, { "parameters": { @@ -116,7 +140,10 @@ "name": "Publish Completion Event", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4, - "position": [1780, 300] + "position": [ + 1780, + 300 + ] }, { "parameters": { @@ -133,7 +160,10 @@ "name": "Has Studio Board ID?", "type": "n8n-nodes-base.if", "typeVersion": 1, - "position": [1780, 500] + "position": [ + 1780, + 500 + ] }, { "parameters": { @@ -142,59 +172,151 @@ "sendHeaders": true, "headerParameters": { "parameters": [ - {"name": "content-type", "value": "application/json"}, - {"name": "apikey", "value": "={{ $env.SUPABASE_SERVICE_ROLE_KEY }}"}, - {"name": "Authorization", "value": "=Bearer {{$env.SUPABASE_SERVICE_ROLE_KEY}}"} + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "apikey", + "value": "={{ $env.SUPABASE_SERVICE_ROLE_KEY }}" + }, + { + "name": "Authorization", + "value": "=Bearer {{$env.SUPABASE_SERVICE_ROLE_KEY}}" + } ] }, "sendBody": true, "body": "={{ JSON.stringify({ status: 'published', meta: { published_at: new Date().toISOString(), platforms: $node['Collect Results'].json.platforms } }) }}", - "options": {"timeout": 10000} + "options": { + "timeout": 10000 + } }, "id": "update_studio", "name": "Update Studio Board", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4, - "position": [2040, 500] + "position": [ + 2040, + 500 + ] } ], "connections": { "Webhook Trigger": { - "main": [[{"node": "Prepare Publish Request", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Prepare Publish Request", + "type": "main", + "index": 0 + } + ] + ] }, "Prepare Publish Request": { - "main": [[ - {"node": "Build Discord Payload", "type": "main", "index": 0}, - {"node": "Build Twitter Thread", "type": "main", "index": 0} - ]] + "main": [ + [ + { + "node": "Build Discord Payload", + "type": "main", + "index": 0 + }, + { + "node": "Build Twitter Thread", + "type": "main", + "index": 0 + } + ] + ] }, "Build Discord Payload": { - "main": [[{"node": "Post to Discord", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Post to Discord", + "type": "main", + "index": 0 + } + ] + ] }, "Build Twitter Thread": { - "main": [[{"node": "Publish to Twitter?", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Publish to Twitter?", + "type": "main", + "index": 0 + } + ] + ] }, "Publish to Twitter?": { "main": [ - [{"node": "Post to Twitter", "type": "main", "index": 0}], - [{"node": "Collect Results", "type": "main", "index": 0}] + [ + { + "node": "Post to Twitter", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Collect Results", + "type": "main", + "index": 0 + } + ] ] }, "Post to Discord": { - "main": [[{"node": "Collect Results", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Collect Results", + "type": "main", + "index": 0 + } + ] + ] }, "Post to Twitter": { - "main": [[{"node": "Collect Results", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Collect Results", + "type": "main", + "index": 0 + } + ] + ] }, "Collect Results": { - "main": [[ - {"node": "Publish Completion Event", "type": "main", "index": 0}, - {"node": "Has Studio Board ID?", "type": "main", "index": 0} - ]] + "main": [ + [ + { + "node": "Publish Completion Event", + "type": "main", + "index": 0 + }, + { + "node": "Has Studio Board ID?", + "type": "main", + "index": 0 + } + ] + ] }, "Has Studio Board ID?": { "main": [ - [{"node": "Update Studio Board", "type": "main", "index": 0}], + [ + { + "node": "Update Studio Board", + "type": "main", + "index": 0 + } + ], [] ] } @@ -206,9 +328,9 @@ "description": "Multi-platform social publisher: Discord + Twitter/X with studio_board tracking" }, "pinData": null, - "versionId": "social-publisher-v1", + "versionId": "63087df4-4db9-4e52-82c7-023dfd56a347", "versionCounter": 1, "triggerCount": 0, - "tags": ["pmoves", "publishing", "discord", "twitter", "social"] + "tags": [] } ] diff --git a/pmoves/n8n/flows/pmoves_video_analysis.json b/pmoves/n8n/flows/pmoves_video_analysis.json index a51b1a8d71..ec716a7a4e 100644 --- a/pmoves/n8n/flows/pmoves_video_analysis.json +++ b/pmoves/n8n/flows/pmoves_video_analysis.json @@ -14,7 +14,10 @@ "name": "Webhook Trigger", "type": "n8n-nodes-base.webhook", "typeVersion": 1, - "position": [200, 300] + "position": [ + 200, + 300 + ] }, { "parameters": { @@ -24,7 +27,10 @@ "name": "Prepare Video Request", "type": "n8n-nodes-base.function", "typeVersion": 2, - "position": [460, 300] + "position": [ + 460, + 300 + ] }, { "parameters": { @@ -41,7 +47,10 @@ "name": "Transcribe Audio Track", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4, - "position": [720, 100] + "position": [ + 720, + 100 + ] }, { "parameters": { @@ -58,7 +67,10 @@ "name": "Analyze Audio Track", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4, - "position": [720, 300] + "position": [ + 720, + 300 + ] }, { "parameters": { @@ -75,7 +87,10 @@ "name": "Analyze Video Frames", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4, - "position": [720, 500] + "position": [ + 720, + 500 + ] }, { "parameters": { @@ -85,7 +100,10 @@ "name": "Merge All Results", "type": "n8n-nodes-base.function", "typeVersion": 2, - "position": [1020, 300] + "position": [ + 1020, + 300 + ] }, { "parameters": { @@ -95,7 +113,10 @@ "name": "Build CGP Envelope", "type": "n8n-nodes-base.function", "typeVersion": 2, - "position": [1300, 300] + "position": [ + 1300, + 300 + ] }, { "parameters": { @@ -112,7 +133,10 @@ "name": "Index to Hi-RAG", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4, - "position": [1560, 200] + "position": [ + 1560, + 200 + ] }, { "parameters": { @@ -129,7 +153,10 @@ "name": "Publish NATS Event", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4, - "position": [1560, 400] + "position": [ + 1560, + 400 + ] }, { "parameters": { @@ -146,7 +173,10 @@ "name": "Auto Research?", "type": "n8n-nodes-base.if", "typeVersion": 1, - "position": [1820, 300] + "position": [ + 1820, + 300 + ] }, { "parameters": { @@ -163,46 +193,119 @@ "name": "Trigger DeepResearch", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4, - "position": [2080, 200] + "position": [ + 2080, + 200 + ] } ], "connections": { "Webhook Trigger": { - "main": [[{"node": "Prepare Video Request", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Prepare Video Request", + "type": "main", + "index": 0 + } + ] + ] }, "Prepare Video Request": { "main": [ [ - {"node": "Transcribe Audio Track", "type": "main", "index": 0}, - {"node": "Analyze Audio Track", "type": "main", "index": 0}, - {"node": "Analyze Video Frames", "type": "main", "index": 0} + { + "node": "Transcribe Audio Track", + "type": "main", + "index": 0 + }, + { + "node": "Analyze Audio Track", + "type": "main", + "index": 0 + }, + { + "node": "Analyze Video Frames", + "type": "main", + "index": 0 + } ] ] }, "Transcribe Audio Track": { - "main": [[{"node": "Merge All Results", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Merge All Results", + "type": "main", + "index": 0 + } + ] + ] }, "Analyze Audio Track": { - "main": [[{"node": "Merge All Results", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Merge All Results", + "type": "main", + "index": 0 + } + ] + ] }, "Analyze Video Frames": { - "main": [[{"node": "Merge All Results", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Merge All Results", + "type": "main", + "index": 0 + } + ] + ] }, "Merge All Results": { - "main": [[{"node": "Build CGP Envelope", "type": "main", "index": 0}]] + "main": [ + [ + { + "node": "Build CGP Envelope", + "type": "main", + "index": 0 + } + ] + ] }, "Build CGP Envelope": { "main": [ [ - {"node": "Index to Hi-RAG", "type": "main", "index": 0}, - {"node": "Publish NATS Event", "type": "main", "index": 0}, - {"node": "Auto Research?", "type": "main", "index": 0} + { + "node": "Index to Hi-RAG", + "type": "main", + "index": 0 + }, + { + "node": "Publish NATS Event", + "type": "main", + "index": 0 + }, + { + "node": "Auto Research?", + "type": "main", + "index": 0 + } ] ] }, "Auto Research?": { "main": [ - [{"node": "Trigger DeepResearch", "type": "main", "index": 0}], + [ + { + "node": "Trigger DeepResearch", + "type": "main", + "index": 0 + } + ], [] ] } @@ -214,9 +317,9 @@ "description": "Video analysis pipeline: transcription + audio analysis + video frame analysis (YOLO/BLIP) + Hi-RAG indexing" }, "pinData": null, - "versionId": "video-analysis-v1", + "versionId": "d538835a-9758-4917-b70e-13153a4f1974", "versionCounter": 1, "triggerCount": 0, - "tags": ["pmoves", "video", "analysis", "multimodal"] + "tags": [] } ] diff --git a/pmoves/n8n/flows/telegram_voice_agent.json b/pmoves/n8n/flows/telegram_voice_agent.json index 86229b0f05..f61e4faf81 100644 --- a/pmoves/n8n/flows/telegram_voice_agent.json +++ b/pmoves/n8n/flows/telegram_voice_agent.json @@ -19,10 +19,10 @@ "webhookId": "telegram-voice-webhook" }, { - "parameters": { - "mode": "rules", - "rules": { - "values": [ + "parameters": { + "mode": "rules", + "rules": { + "values": [ { "conditions": { "options": { @@ -443,19 +443,6 @@ "versionId": "9b90a1d1-c767-428d-95c3-c1ff0cbec1c9", "versionCounter": 4, "triggerCount": 0, - "tags": [ - { - "updatedAt": "2025-12-12T13:18:03.530Z", - "createdAt": "2025-12-12T13:18:03.530Z", - "id": "0A3B8qR44ylHUdFz", - "name": "voice-agent" - }, - { - "updatedAt": "2025-12-12T13:18:07.141Z", - "createdAt": "2025-12-12T13:18:07.141Z", - "id": "qqizk7QbcLVI2tC1", - "name": "telegram" - } - ] + "tags": [] } ] diff --git a/pmoves/n8n/flows/voice_platform_router.json b/pmoves/n8n/flows/voice_platform_router.json index d3c5292e09..58cb2c373d 100644 --- a/pmoves/n8n/flows/voice_platform_router.json +++ b/pmoves/n8n/flows/voice_platform_router.json @@ -21,7 +21,7 @@ }, { "parameters": { - "jsCode": "// Unified platform normalizer\n// Input: raw webhook payload from any platform\n// Output: normalized voice message object\n\nconst items = $input.all();\nconst raw = items[0].json;\n// Webhook nodes typically nest the request body under `body`\nconst payload = raw.body ?? raw;\n\nlet normalized = {\n platform: null,\n user_id: null,\n user_name: null,\n channel_id: null,\n message_id: null,\n message_type: 'text', // 'text' | 'voice'\n content: null,\n audio_url: null,\n timestamp: new Date().toISOString(),\n raw_payload: raw,\n body: payload\n};\n\n// Detect platform and normalize\nif (payload.platform) {\n // Already normalized (direct API call)\n normalized = { ...normalized, ...payload };\n} else if (payload.message?.chat?.id && payload.message?.from?.id) {\n // Telegram format\n normalized.platform = 'telegram';\n normalized.user_id = String(payload.message.from.id);\n normalized.user_name = payload.message.from.first_name || payload.message.from.username;\n normalized.channel_id = String(payload.message.chat.id);\n normalized.message_id = String(payload.message.message_id);\n \n if (payload.message.voice) {\n normalized.message_type = 'voice';\n normalized.audio_url = payload.message.voice.file_id; // Needs Telegram API to resolve\n } else if (payload.message.text) {\n normalized.message_type = 'text';\n normalized.content = payload.message.text;\n }\n} else if (payload.messages?.[0]?.from) {\n // WhatsApp format\n normalized.platform = 'whatsapp';\n normalized.user_id = payload.messages[0].from;\n normalized.user_name = payload.contacts?.[0]?.profile?.name || 'WhatsApp User';\n normalized.channel_id = payload.metadata?.phone_number_id;\n normalized.message_id = payload.messages[0].id;\n \n if (payload.messages[0].type === 'audio') {\n normalized.message_type = 'voice';\n normalized.audio_url = payload.messages[0].audio?.id; // Needs WhatsApp API to resolve\n } else if (payload.messages[0].type === 'text') {\n normalized.message_type = 'text';\n normalized.content = payload.messages[0].text?.body;\n }\n} else if (payload.author?.id && payload.channelId) {\n // Discord format\n normalized.platform = 'discord';\n normalized.user_id = payload.author.id;\n normalized.user_name = payload.author.username;\n normalized.channel_id = payload.channelId;\n normalized.message_id = payload.id;\n \n if (payload.attachments?.length > 0 && payload.attachments[0].contentType?.startsWith('audio/')) {\n normalized.message_type = 'voice';\n normalized.audio_url = payload.attachments[0].url;\n } else if (payload.content) {\n normalized.message_type = 'text';\n normalized.content = payload.content;\n }\n}\n\n// Ensure a stable message_id for contract publishing\nif (!normalized.message_id) {\n try {\n normalized.message_id = `local-${crypto.randomUUID()}`;\n } catch {\n normalized.message_id = `local-${Date.now()}-${Math.random().toString(16).slice(2)}`;\n }\n}\n\n// Validate required fields\nif (!normalized.platform || !normalized.user_id) {\n throw new Error('Unable to detect platform or extract user_id from payload');\n}\n\nreturn [{ json: normalized }];" + "jsCode": "// Unified platform normalizer\n// Input: raw webhook payload from any platform\n// Output: normalized voice message object\n\nconst items = $input.all();\nconst raw = items[0].json;\n// Webhook nodes typically nest the request body under `body`\nconst payload = raw.body ?? raw;\n\nlet normalized = {\n platform: null,\n user_id: null,\n user_name: null,\n channel_id: null,\n message_id: null,\n message_type: 'text', // 'text' | 'voice'\n content: null,\n audio_url: null,\n timestamp: new Date().toISOString(),\n raw_payload: raw,\n body: payload\n};\n\n// Detect platform and normalize\nif (payload.platform) {\n // Already normalized (direct API call)\n normalized = { ...normalized, ...payload };\n} else if (payload.message?.chat?.id && payload.message?.from?.id) {\n // Telegram format\n normalized.platform = 'telegram';\n normalized.user_id = String(payload.message.from.id);\n normalized.user_name = payload.message.from.first_name || payload.message.from.username;\n normalized.channel_id = String(payload.message.chat.id);\n normalized.message_id = String(payload.message.message_id);\n \n if (payload.message.voice) {\n normalized.message_type = 'voice';\n normalized.audio_url = payload.message.voice.file_id; // Needs Telegram API to resolve\n } else if (payload.message.text) {\n normalized.message_type = 'text';\n normalized.content = payload.message.text;\n }\n} else if (payload.messages?.[0]?.from) {\n // WhatsApp format\n normalized.platform = 'whatsapp';\n normalized.user_id = payload.messages[0].from;\n normalized.user_name = payload.contacts?.[0]?.profile?.name || 'WhatsApp User';\n normalized.channel_id = payload.metadata?.phone_number_id;\n normalized.message_id = payload.messages[0].id;\n \n if (payload.messages[0].type === 'audio') {\n normalized.message_type = 'voice';\n normalized.audio_url = payload.messages[0].audio?.id; // Needs WhatsApp API to resolve\n } else if (payload.messages[0].type === 'text') {\n normalized.message_type = 'text';\n normalized.content = payload.messages[0].text?.body;\n }\n} else if (payload.author?.id && payload.channelId) {\n // Discord format\n normalized.platform = 'discord';\n normalized.user_id = payload.author.id;\n normalized.user_name = payload.author.username;\n normalized.channel_id = payload.channelId;\n normalized.message_id = payload.id;\n \n if (payload.attachments?.length > 0 && payload.attachments[0].contentType?.startsWith('audio/')) {\n normalized.message_type = 'voice';\n normalized.audio_url = payload.attachments[0].url;\n } else if (payload.content) {\n normalized.message_type = 'text';\n normalized.content = payload.content;\n }\n}\n\n// Ensure a stable message_id for contract publishing\nif (!normalized.message_id) {\n try {\n normalized.message_id = `local-${crypto.randomUUID()}`;\n } catch {\n normalized.message_id = `local-${Date.now()}-${Math.random().toString(16).slice(2)}`;\n }\n}\n\n// Validate required fields\nif (!normalized.platform || !normalized.user_id) {\n throw new Error('Unable to detect platform or extract user_id from payload');\n}\n\nreturn [{ json: normalized }];" }, "id": "normalize-platform", "name": "Normalize Platform", @@ -191,14 +191,14 @@ 900, 100 ], - "credentials": { - "telegramApi": { - "id": "telegram-bot-cred", - "name": "Telegram Bot" - } - }, - "disabled": true - }, + "credentials": { + "telegramApi": { + "id": "telegram-bot-cred", + "name": "Telegram Bot" + } + }, + "disabled": true + }, { "parameters": { "method": "GET", @@ -215,12 +215,12 @@ "name": "Telegram Download", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, - "position": [ - 1100, - 100 - ], - "disabled": true - }, + "position": [ + 1100, + 100 + ], + "disabled": true + }, { "parameters": { "operation": "mediaUrlGet", @@ -234,14 +234,14 @@ 900, 200 ], - "credentials": { - "whatsAppBusinessCloudApi": { - "id": "whatsapp-cred", - "name": "WhatsApp Business" - } - }, - "disabled": true - }, + "credentials": { + "whatsAppBusinessCloudApi": { + "id": "whatsapp-cred", + "name": "WhatsApp Business" + } + }, + "disabled": true + }, { "parameters": { "method": "GET", @@ -264,14 +264,14 @@ 1100, 200 ], - "credentials": { - "httpHeaderAuth": { - "id": "whatsapp-auth", - "name": "WhatsApp Token" - } - }, - "disabled": true - }, + "credentials": { + "httpHeaderAuth": { + "id": "whatsapp-auth", + "name": "WhatsApp Token" + } + }, + "disabled": true + }, { "parameters": { "method": "GET", @@ -296,16 +296,16 @@ { "parameters": { "method": "POST", - "url": "http://flute-gateway:8055/v1/voice/recognize", + "url": "http://flute-gateway:8055/v1/voice/recognize", "sendBody": true, "contentType": "multipart-form-data", "bodyParameters": { "parameters": [ { "parameterType": "formBinaryData", - "name": "audio", + "name": "audio", "inputDataFieldName": "data" - } + } ] }, "options": {} @@ -333,17 +333,17 @@ 250 ] }, - { - "parameters": { - "method": "POST", - "url": "http://hi-rag-gateway-v2:8086/hirag/query", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={\n \"query\": \"{{ $json.content }}\",\n \"top_k\": 3,\n \"rerank\": true\n}", - "options": { - "timeout": 15000 - } - }, + { + "parameters": { + "method": "POST", + "url": "http://hi-rag-gateway-v2:8086/hirag/query", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={\n \"query\": \"{{ $json.content }}\",\n \"top_k\": 3,\n \"rerank\": true\n}", + "options": { + "timeout": 15000 + } + }, "id": "hirag-query", "name": "Hi-RAG Query", "type": "n8n-nodes-base.httpRequest", @@ -368,44 +368,43 @@ ] }, { - "parameters": { - "method": "POST", - "url": "={{ ($env.OPENAI_COMPATIBLE_BASE_URL || $env.OPENAI_COMPAT_BASE_URL || ($env.TENSORZERO_BASE_URL ? ($env.TENSORZERO_BASE_URL.replace(/\\/$/, '') + '/openai/v1') : 'https://api.openai.com/v1')).replace(/\\/$/, '') }}/chat/completions", - "sendHeaders": true, - "headerParameters": { - "parameters": [ - { - "name": "content-type", - "value": "application/json" - }, - { - "name": "Authorization", - "value": "={{ 'Bearer ' + ($env.OPENAI_COMPAT_API_KEY || $env.OPENAI_API_KEY || '') }}" - } - ] - }, - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ {\n model: ($env.VOICE_AGENT_MODEL || $env.OPENAI_MODEL || ($env.TENSORZERO_BASE_URL ? 'tensorzero::model_name::qwen2_5_14b' : 'gpt-4o-mini')),\n messages: [\n {\n role: 'system',\n content: 'You are a helpful voice assistant for PMOVES.AI. Keep responses concise and conversational, suitable for voice output. Aim for 1-3 sentences unless more detail is specifically requested.' + ($json.context || '')\n },\n {\n role: 'user',\n content: $json.content\n }\n ],\n max_tokens: 500\n} }}", - "options": { - "timeout": 180000 - } - }, + "parameters": { + "method": "POST", + "url": "={{ ($env.OPENAI_COMPATIBLE_BASE_URL || $env.OPENAI_COMPAT_BASE_URL || ($env.TENSORZERO_BASE_URL ? ($env.TENSORZERO_BASE_URL.replace(/\\/$/, '') + '/openai/v1') : 'https://api.openai.com/v1')).replace(/\\/$/, '') }}/chat/completions", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "Authorization", + "value": "={{ 'Bearer ' + ($env.OPENAI_COMPAT_API_KEY || $env.OPENAI_API_KEY || '') }}" + } + ] + }, + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ {\n model: ($env.VOICE_AGENT_MODEL || $env.OPENAI_MODEL || ($env.TENSORZERO_BASE_URL ? 'tensorzero::model_name::qwen2_5_14b' : 'gpt-4o-mini')),\n messages: [\n {\n role: 'system',\n content: 'You are a helpful voice assistant for PMOVES.AI. Keep responses concise and conversational, suitable for voice output. Aim for 1-3 sentences unless more detail is specifically requested.' + ($json.context || '')\n },\n {\n role: 'user',\n content: $json.content\n }\n ],\n max_tokens: 500\n} }}", + "options": { + "timeout": 180000 + } + }, "id": "llm-response", "name": "Generate Response", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, - "position": [ - 2100, - 300 - ] - , - "continueOnFail": true, - "alwaysOutputData": true - }, + "position": [ + 2100, + 300 + ], + "continueOnFail": true, + "alwaysOutputData": true + }, { "parameters": { - "jsCode": "// Extract response and prepare for platform routing\nconst items = $input.all();\nconst rawResult = items[0].json;\nconst llmResult = rawResult.body ?? rawResult;\nconst prevData = $('Build Context').first().json;\n\nlet responseText = llmResult.choices?.[0]?.message?.content || llmResult.response || llmResult.text;\n\n// Fallback: if the LLM is unavailable, return the top RAG context snippet\nif (!responseText) {\n const ctx = String(prevData.context || '');\n const m = ctx.match(/\\[1\\]\\s*([^\\n]+)/);\n if (m && m[1] && m[1].trim()) {\n responseText = m[1].trim();\n }\n}\n\nif (!responseText) {\n responseText = 'Sorry, I could not generate a response.';\n}\n\nconst defaultModel = ($env.VOICE_AGENT_MODEL || $env.OPENAI_MODEL || ($env.TENSORZERO_BASE_URL ? 'tensorzero::model_name::qwen2_5_14b' : 'gpt-4o-mini'));\n\nreturn [{\n json: {\n ...prevData,\n response_text: responseText,\n model_used: llmResult.model || rawResult.model || defaultModel\n }\n}];" + "jsCode": "// Extract response and prepare for platform routing\nconst items = $input.all();\nconst rawResult = items[0].json;\nconst llmResult = rawResult.body ?? rawResult;\nconst prevData = $('Build Context').first().json;\n\nlet responseText = llmResult.choices?.[0]?.message?.content || llmResult.response || llmResult.text;\n\n// Fallback: if the LLM is unavailable, return the top RAG context snippet\nif (!responseText) {\n const ctx = String(prevData.context || '');\n const m = ctx.match(/\\[1\\]\\s*([^\\n]+)/);\n if (m && m[1] && m[1].trim()) {\n responseText = m[1].trim();\n }\n}\n\nif (!responseText) {\n responseText = 'Sorry, I could not generate a response.';\n}\n\nconst defaultModel = ($env.VOICE_AGENT_MODEL || $env.OPENAI_MODEL || ($env.TENSORZERO_BASE_URL ? 'tensorzero::model_name::qwen2_5_14b' : 'gpt-4o-mini'));\n\nreturn [{\n json: {\n ...prevData,\n response_text: responseText,\n model_used: llmResult.model || rawResult.model || defaultModel\n }\n}];" }, "id": "extract-response", "name": "Extract Response", @@ -416,13 +415,13 @@ 300 ] }, - { - "parameters": { - "mode": "rules", - "rules": { - "values": [ - { - "conditions": { + { + "parameters": { + "mode": "rules", + "rules": { + "values": [ + { + "conditions": { "options": { "leftValue": "", "caseSensitive": true, @@ -484,35 +483,35 @@ } ] }, - "renameOutput": true, - "outputKey": "discord" - }, - { - "conditions": { - "options": { - "leftValue": "", - "caseSensitive": true, - "typeValidation": "strict" - }, - "combinator": "and", - "conditions": [ - { - "leftValue": "={{ $json.platform }}", - "rightValue": "local", - "operator": { - "type": "string", - "operation": "equals" - } - } - ] - }, - "renameOutput": true, - "outputKey": "local" - } - ] - }, - "options": {} - }, + "renameOutput": true, + "outputKey": "discord" + }, + { + "conditions": { + "options": { + "leftValue": "", + "caseSensitive": true, + "typeValidation": "strict" + }, + "combinator": "and", + "conditions": [ + { + "leftValue": "={{ $json.platform }}", + "rightValue": "local", + "operator": { + "type": "string", + "operation": "equals" + } + } + ] + }, + "renameOutput": true, + "outputKey": "local" + } + ] + }, + "options": {} + }, "id": "route-platform-response", "name": "Route Platform (Response)", "type": "n8n-nodes-base.switch", @@ -538,14 +537,14 @@ 2700, 200 ], - "credentials": { - "telegramApi": { - "id": "telegram-bot-cred", - "name": "Telegram Bot" - } - }, - "disabled": true - }, + "credentials": { + "telegramApi": { + "id": "telegram-bot-cred", + "name": "Telegram Bot" + } + }, + "disabled": true + }, { "parameters": { "operation": "sendMessage", @@ -561,39 +560,39 @@ 2700, 300 ], - "credentials": { - "whatsAppBusinessCloudApi": { - "id": "whatsapp-cred", - "name": "WhatsApp Business" - } - }, - "disabled": true - }, - { - "parameters": { - "method": "POST", - "url": "http://messaging-gateway:8101/v1/send", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={\n \"platforms\": [\"discord\"],\n \"content\": \"{{ $json.response_text }}\",\n \"meta\": {\n \"channel_id\": \"{{ $json.channel_id }}\",\n \"message_id\": \"{{ $json.message_id }}\",\n \"user_id\": \"{{ $json.user_id }}\",\n \"user_name\": \"{{ $json.user_name }}\"\n }\n}", - "options": { - "timeout": 10000 - } - }, - "id": "send-discord", - "name": "Send Discord", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 2700, - 400 - ], - "continueOnFail": true - }, + "credentials": { + "whatsAppBusinessCloudApi": { + "id": "whatsapp-cred", + "name": "WhatsApp Business" + } + }, + "disabled": true + }, + { + "parameters": { + "method": "POST", + "url": "http://messaging-gateway:8101/v1/send", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={\n \"platforms\": [\"discord\"],\n \"content\": \"{{ $json.response_text }}\",\n \"meta\": {\n \"channel_id\": \"{{ $json.channel_id }}\",\n \"message_id\": \"{{ $json.message_id }}\",\n \"user_id\": \"{{ $json.user_id }}\",\n \"user_name\": \"{{ $json.user_name }}\"\n }\n}", + "options": { + "timeout": 10000 + } + }, + "id": "send-discord", + "name": "Send Discord", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 2700, + 400 + ], + "continueOnFail": true + }, { - "parameters": { - "method": "POST", - "url": "={{ $env.SUPABASE_URL }}/rest/v1/voice_messages", + "parameters": { + "method": "POST", + "url": "={{ $env.SUPABASE_URL }}/rest/v1/voice_messages", "sendHeaders": true, "headerParameters": { "parameters": [ @@ -615,13 +614,13 @@ } ] }, - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={\n \"platform\": \"{{ $json.platform }}\",\n \"user_id\": \"{{ $json.user_id }}\",\n \"user_name\": \"{{ $json.user_name }}\",\n \"transcript\": \"{{ $json.content }}\",\n \"response_text\": \"{{ $json.response_text }}\",\n \"model_used\": \"{{ $json.model_used }}\",\n \"status\": \"completed\",\n \"metadata\": { \"message_type\": \"{{ $json.message_type }}\", \"sources\": {{ JSON.stringify($json.sources || []) }} }\n}", - "options": { - "timeout": 10000 - } - }, + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={\n \"platform\": \"{{ $json.platform }}\",\n \"user_id\": \"{{ $json.user_id }}\",\n \"user_name\": \"{{ $json.user_name }}\",\n \"transcript\": \"{{ $json.content }}\",\n \"response_text\": \"{{ $json.response_text }}\",\n \"model_used\": \"{{ $json.model_used }}\",\n \"status\": \"completed\",\n \"metadata\": { \"message_type\": \"{{ $json.message_type }}\", \"sources\": {{ JSON.stringify($json.sources || []) }} }\n}", + "options": { + "timeout": 10000 + } + }, "id": "log-to-supabase", "name": "Log to Supabase", "type": "n8n-nodes-base.httpRequest", @@ -632,70 +631,70 @@ ], "continueOnFail": true }, - { - "parameters": { - "method": "POST", - "url": "={{ $env.AGENT_ZERO_BASE_URL || 'http://agent-zero:8080' }}/events/publish", - "sendHeaders": true, - "headerParameters": { - "parameters": [ - { - "name": "content-type", - "value": "application/json" - }, - { - "name": "x-agent-token", - "value": "={{ $env.AGENT_ZERO_EVENTS_TOKEN || '' }}" - } - ] - }, - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={\n \"topic\": \"voice.agent.response.v1\",\n \"payload\": {\n \"platform\": \"{{ $('Extract Response').first().json.platform }}\",\n \"user_id\": \"{{ $('Extract Response').first().json.user_id }}\",\n \"message_id\": \"{{ $('Extract Response').first().json.message_id }}\",\n \"response_text\": \"{{ $('Extract Response').first().json.response_text }}\",\n \"model_used\": \"{{ $('Extract Response').first().json.model_used }}\",\n \"timestamp\": \"{{ $('Extract Response').first().json.timestamp }}\",\n \"sources\": {{ JSON.stringify($('Extract Response').first().json.sources || []) }}\n }\n}", - "options": { - "timeout": 10000 - } - }, - "id": "publish-nats", - "name": "Publish NATS Event", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [ - 3100, - 300 - ], - "continueOnFail": true - }, - { - "parameters": { - "jsCode": "// Build a stable webhook response payload.\n// Prefer the pre-publish data from Extract Response; include publish result when available.\n\nlet base;\ntry {\n base = $('Extract Response').first().json;\n} catch {\n base = $input.first().json;\n}\n\nconst publishResult = $json || {};\n\nreturn [\n {\n json: {\n success: true,\n platform: base.platform,\n user_id: base.user_id,\n user_name: base.user_name,\n message_id: base.message_id,\n message_type: base.message_type,\n response_text: base.response_text,\n model_used: base.model_used,\n sources: base.sources || [],\n published_topic: publishResult.published || null,\n published_id: publishResult.id || null,\n publish_ok: Boolean(publishResult.published)\n }\n }\n];" - }, - "id": "build-webhook-response", - "name": "Build Webhook Response", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 3300, - 300 - ] - }, - { - "parameters": { - "respondWith": "json", - "responseBody": "={{ $json }}", - "options": {} - }, - "id": "respond-webhook", - "name": "Respond to Webhook", - "type": "n8n-nodes-base.respondToWebhook", - "typeVersion": 1.1, - "position": [ - 3500, - 300 - ] - } - ], - "connections": { + { + "parameters": { + "method": "POST", + "url": "={{ $env.AGENT_ZERO_BASE_URL || 'http://agent-zero:8080' }}/events/publish", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "x-agent-token", + "value": "={{ $env.AGENT_ZERO_EVENTS_TOKEN || '' }}" + } + ] + }, + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={\n \"topic\": \"voice.agent.response.v1\",\n \"payload\": {\n \"platform\": \"{{ $('Extract Response').first().json.platform }}\",\n \"user_id\": \"{{ $('Extract Response').first().json.user_id }}\",\n \"message_id\": \"{{ $('Extract Response').first().json.message_id }}\",\n \"response_text\": \"{{ $('Extract Response').first().json.response_text }}\",\n \"model_used\": \"{{ $('Extract Response').first().json.model_used }}\",\n \"timestamp\": \"{{ $('Extract Response').first().json.timestamp }}\",\n \"sources\": {{ JSON.stringify($('Extract Response').first().json.sources || []) }}\n }\n}", + "options": { + "timeout": 10000 + } + }, + "id": "publish-nats", + "name": "Publish NATS Event", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [ + 3100, + 300 + ], + "continueOnFail": true + }, + { + "parameters": { + "jsCode": "// Build a stable webhook response payload.\n// Prefer the pre-publish data from Extract Response; include publish result when available.\n\nlet base;\ntry {\n base = $('Extract Response').first().json;\n} catch {\n base = $input.first().json;\n}\n\nconst publishResult = $json || {};\n\nreturn [\n {\n json: {\n success: true,\n platform: base.platform,\n user_id: base.user_id,\n user_name: base.user_name,\n message_id: base.message_id,\n message_type: base.message_type,\n response_text: base.response_text,\n model_used: base.model_used,\n sources: base.sources || [],\n published_topic: publishResult.published || null,\n published_id: publishResult.id || null,\n publish_ok: Boolean(publishResult.published)\n }\n }\n];" + }, + "id": "build-webhook-response", + "name": "Build Webhook Response", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 3300, + 300 + ] + }, + { + "parameters": { + "respondWith": "json", + "responseBody": "={{ $json }}", + "options": {} + }, + "id": "respond-webhook", + "name": "Respond to Webhook", + "type": "n8n-nodes-base.respondToWebhook", + "typeVersion": 1.1, + "position": [ + 3500, + 300 + ] + } + ], + "connections": { "Webhook Trigger": { "main": [ [ @@ -882,38 +881,38 @@ ] ] }, - "Route Platform (Response)": { - "main": [ - [ - { - "node": "Send Telegram", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Send WhatsApp", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Send Discord", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Log to Supabase", - "type": "main", - "index": 0 - } - ] - ] - }, + "Route Platform (Response)": { + "main": [ + [ + { + "node": "Send Telegram", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Send WhatsApp", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Send Discord", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Log to Supabase", + "type": "main", + "index": 0 + } + ] + ] + }, "Send Telegram": { "main": [ [ @@ -958,30 +957,29 @@ ] ] }, - "Publish NATS Event": { - "main": [ - [ - { - "node": "Build Webhook Response", - "type": "main", - "index": 0 - } - ] - ] - } - , - "Build Webhook Response": { - "main": [ - [ - { - "node": "Respond to Webhook", - "type": "main", - "index": 0 - } - ] - ] - } - }, + "Publish NATS Event": { + "main": [ + [ + { + "node": "Build Webhook Response", + "type": "main", + "index": 0 + } + ] + ] + }, + "Build Webhook Response": { + "main": [ + [ + { + "node": "Respond to Webhook", + "type": "main", + "index": 0 + } + ] + ] + } + }, "settings": { "executionOrder": "v1" }, @@ -994,25 +992,6 @@ "versionId": "9fe3a9e9-9cdc-4da6-8774-198b9e3d8219", "versionCounter": 4, "triggerCount": 0, - "tags": [ - { - "updatedAt": "2025-12-12T13:18:03.530Z", - "createdAt": "2025-12-12T13:18:03.530Z", - "id": "0A3B8qR44ylHUdFz", - "name": "voice-agent" - }, - { - "updatedAt": "2025-12-12T13:18:08.905Z", - "createdAt": "2025-12-12T13:18:08.905Z", - "id": "caJaL27AnB331apC", - "name": "router" - }, - { - "updatedAt": "2025-12-12T13:18:08.906Z", - "createdAt": "2025-12-12T13:18:08.906Z", - "id": "vPCBPRTNV0H4pHxT", - "name": "unified" - } - ] + "tags": [] } ] diff --git a/pmoves/n8n/flows/voice_shared_functions.json b/pmoves/n8n/flows/voice_shared_functions.json index 8fec73f75b..5044c968fe 100644 --- a/pmoves/n8n/flows/voice_shared_functions.json +++ b/pmoves/n8n/flows/voice_shared_functions.json @@ -79,10 +79,10 @@ "webhookId": "voice-tts" }, { - "parameters": { - "mode": "rules", - "rules": { - "values": [ + "parameters": { + "mode": "rules", + "rules": { + "values": [ { "conditions": { "options": { @@ -505,25 +505,6 @@ "versionId": "ea1b721c-4be4-40d4-a19a-0a7e4de2e12f", "versionCounter": 4, "triggerCount": 0, - "tags": [ - { - "updatedAt": "2025-12-12T13:18:03.530Z", - "createdAt": "2025-12-12T13:18:03.530Z", - "id": "0A3B8qR44ylHUdFz", - "name": "voice-agent" - }, - { - "updatedAt": "2025-12-12T13:18:03.534Z", - "createdAt": "2025-12-12T13:18:03.534Z", - "id": "pmKflb8eoRUftfSk", - "name": "functions" - }, - { - "updatedAt": "2025-12-12T13:18:03.533Z", - "createdAt": "2025-12-12T13:18:03.533Z", - "id": "uWvnDIE0g0A354hz", - "name": "shared" - } - ] + "tags": [] } ] From 6e9f2ce2f46d97ee2184cde87204c545b2cc1b01 Mon Sep 17 00:00:00 2001 From: Shaela Bello Date: Thu, 12 Mar 2026 06:28:05 -0400 Subject: [PATCH 2/3] feat(n8n): default to postgres control plane and track workflows --- pmoves/Makefile | 41 +++++++++++--- pmoves/docker-compose.n8n.postgres.yml | 3 +- pmoves/docker-compose.n8n.yml | 5 +- pmoves/docs/NEXT_STEPS.md | 8 +++ pmoves/docs/PMOVES.AI PLANS/MAKE_TARGETS.md | 13 +++++ pmoves/docs/PMOVES.AI PLANS/N8N_SETUP.md | 56 ++++++++++++------- pmoves/docs/PMOVES.AI PLANS/ROADMAP.md | 6 ++ pmoves/env.shared.example | 15 +++++ pmoves/n8n/README.md | 2 + .../20260312130000_n8n_workflow_registry.sql | 49 ++++++++++++++++ pmoves/tools/brand_defaults.py | 3 +- 11 files changed, 169 insertions(+), 32 deletions(-) create mode 100644 pmoves/supabase/migrations/20260312130000_n8n_workflow_registry.sql diff --git a/pmoves/Makefile b/pmoves/Makefile index 4b85f77b92..cc6f52922b 100644 --- a/pmoves/Makefile +++ b/pmoves/Makefile @@ -1417,9 +1417,22 @@ N8N_SUBMODULE_DIR ?= ../PMOVES-n8n N8N_CANONICAL_FLOWS_DIR ?= $(N8N_SUBMODULE_DIR)/workflows # n8n persistence mode: -# - sqlite: simplest local bring-up (default) -# - postgres: production-grade n8n DB (recommended for VPS) -N8N_DB ?= sqlite +# - postgres: production-grade n8n DB (default) +# - sqlite: legacy/local escape hatch only +N8N_DB ?= postgres +N8N_DB_NAME ?= n8n +N8N_DB_USER ?= n8n +N8N_DB_PASSWORD ?= pmoves_n8n_local +N8N_DB_SCHEMA ?= public +N8N_BASE_URL ?= http://localhost:5678 +N8N_API_URL ?= $(N8N_BASE_URL)/api/v1 +N8N_OWNER_EMAIL ?= $(if $(PMOVES_OPERATOR_EMAIL),$(PMOVES_OPERATOR_EMAIL),$(SUPABASE_BOOT_USER_EMAIL)) +N8N_OWNER_FIRST_NAME ?= PMOVES +N8N_OWNER_LAST_NAME ?= Operator +N8N_API_KEY_LABEL ?= PMOVES.AI automation bootstrap +N8N_ENV_WRITE_FILE ?= .env.local +export N8N_DB N8N_DB_NAME N8N_DB_USER N8N_DB_PASSWORD N8N_DB_SCHEMA +export N8N_BASE_URL N8N_API_URL N8N_OWNER_EMAIL N8N_OWNER_FIRST_NAME N8N_OWNER_LAST_NAME N8N_API_KEY_LABEL ifeq ($(N8N_DB),postgres) N8N_DB_STACK_FILE := -f docker-compose.n8n.postgres.yml else @@ -2346,12 +2359,13 @@ up-agents-integrations: ## Start agents using your forks (builds from $(INTEGRAT @$(DC) -f docker-compose.agents.integrations.yml --profile agents up -d nats agent-zero archon archon-ui mesh-agent publisher-discord @echo "✔ Agents started from integrations workspace. Workspace: $(INTEGRATIONS_WORKSPACE)" -.PHONY: up-n8n n8n-sync-submodule-flows n8n-import-flows n8n-activate-flows n8n-bootstrap n8n-export-repo-flows +.PHONY: up-n8n n8n-sync-submodule-flows n8n-api-bootstrap n8n-import-flows n8n-activate-flows n8n-sync-supabase-registry n8n-bootstrap n8n-export-repo-flows n8n-sync-submodule-flows: ## Mirror canonical PMOVES-n8n workflows into pmoves/n8n/flows @$(PYTHON) -c "from pathlib import Path; import shutil; src=Path(r'$(N8N_CANONICAL_FLOWS_DIR)'); dst=Path(r'$(CURDIR)/n8n/flows'); dst.mkdir(parents=True, exist_ok=True); [shutil.copy2(path, dst / path.name) for path in sorted(src.glob('*.json'))]" @echo "✔ Mirrored PMOVES-n8n workflows into pmoves/n8n/flows" up-n8n: ensure-env-shared n8n-sync-submodule-flows + @docker network create cataclysm-net >/dev/null 2>&1 || true @if [ "$(N8N_DB)" = "postgres" ]; then \ echo "→ n8n DB mode: postgres"; \ $(N8N_DC) up -d n8n-db n8n n8n-runners; \ @@ -2365,13 +2379,26 @@ down-n8n: ## Stop n8n workflow services @$(N8N_DC) stop n8n n8n-runners n8n-db >/dev/null 2>&1 || true @$(N8N_DC) rm -f n8n n8n-runners n8n-db >/dev/null 2>&1 || true +n8n-api-bootstrap: ## Create/login the n8n owner and mint a fresh Public API key into the local env file + @$(LOAD_ENV_SHARED); $(PYTHON) "$(N8N_SUBMODULE_DIR)/scripts/bootstrap_n8n_api.py" \ + --base-url "$(N8N_BASE_URL)" \ + --api-url "$(N8N_API_URL)" \ + --email "$(N8N_OWNER_EMAIL)" \ + --first-name "$(N8N_OWNER_FIRST_NAME)" \ + --last-name "$(N8N_OWNER_LAST_NAME)" \ + --label "$(N8N_API_KEY_LABEL)" \ + --write-env "$(N8N_ENV_WRITE_FILE)" + n8n-import-flows: ## Import canonical PMOVES-n8n workflows into the live n8n instance - @$(PYTHON) "$(N8N_SUBMODULE_DIR)/scripts/import_repo_flows.py" --container pmoves-n8n --workflow-dir "$(N8N_CANONICAL_FLOWS_DIR)" + @$(LOAD_ENV_SHARED); $(PYTHON) "$(N8N_SUBMODULE_DIR)/scripts/import_repo_flows.py" --container pmoves-n8n --workflow-dir "$(N8N_CANONICAL_FLOWS_DIR)" --skip-activation n8n-activate-flows: ## Publish canonical PMOVES-n8n workflows (keeps chat-platform voice flows inactive unless VOICE_PLATFORMS=1) - @$(PYTHON) "$(N8N_SUBMODULE_DIR)/scripts/import_repo_flows.py" --container pmoves-n8n --workflow-dir "$(N8N_CANONICAL_FLOWS_DIR)" --activate-only $(if $(VOICE_PLATFORMS),--voice-platforms,) + @$(LOAD_ENV_SHARED); $(PYTHON) "$(N8N_SUBMODULE_DIR)/scripts/import_repo_flows.py" --container pmoves-n8n --workflow-dir "$(N8N_CANONICAL_FLOWS_DIR)" --activate-only $(if $(VOICE_PLATFORMS),--voice-platforms,) + +n8n-sync-supabase-registry: ## Upsert live n8n workflow state into pmoves_core.n8n_workflow_registry + @$(LOAD_ENV_SHARED); $(PYTHON) "$(N8N_SUBMODULE_DIR)/scripts/sync_supabase_registry.py" --workflow-dir "$(N8N_CANONICAL_FLOWS_DIR)" -n8n-bootstrap: up-n8n n8n-import-flows n8n-activate-flows ## Bring up n8n, import workflows, and publish the default set +n8n-bootstrap: up-n8n n8n-api-bootstrap n8n-import-flows n8n-activate-flows n8n-sync-supabase-registry ## Bring up n8n, bootstrap API access, import workflows, publish defaults, and sync Supabase tracking @echo "✔ n8n bootstrap complete" n8n-export-repo-flows: ## Export live n8n workflows into PMOVES-n8n and refresh the compatibility mirror diff --git a/pmoves/docker-compose.n8n.postgres.yml b/pmoves/docker-compose.n8n.postgres.yml index 3c29ceaa8b..b8bd1bfba9 100644 --- a/pmoves/docker-compose.n8n.postgres.yml +++ b/pmoves/docker-compose.n8n.postgres.yml @@ -10,7 +10,7 @@ services: - POSTGRES_PASSWORD=${N8N_DB_PASSWORD:?set N8N_DB_PASSWORD} volumes: - n8n-db-data:/var/lib/postgresql/data - networks: [cataclysm, api_tier] + networks: [cataclysm] healthcheck: test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"] interval: 5s @@ -32,4 +32,3 @@ services: volumes: n8n-db-data: {} - diff --git a/pmoves/docker-compose.n8n.yml b/pmoves/docker-compose.n8n.yml index fc46fcc65b..f1ef2d2c9a 100644 --- a/pmoves/docker-compose.n8n.yml +++ b/pmoves/docker-compose.n8n.yml @@ -9,7 +9,7 @@ services: build: context: ../PMOVES-n8n dockerfile: compose/n8n/Dockerfile - image: pmoves/n8n:2.1.0-sqlite + image: pmoves/n8n:2.1.0-runtime container_name: pmoves-n8n restart: unless-stopped <<: *env-tier-worker @@ -17,6 +17,7 @@ services: - N8N_PORT=5678 - N8N_PROTOCOL=http - N8N_HOST=localhost + - N8N_SECURE_COOKIE=false - N8N_DEFAULT_TIMEZONE=${TZ:-America/New_York} - WEBHOOK_URL=http://localhost:5678 - GENERIC_TIMEZONE=${TZ:-America/New_York} @@ -45,6 +46,7 @@ services: - N8N_LOG_LEVEL=debug - N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true ports: ["5678:5678"] + networks: [cataclysm] extra_hosts: - "host.docker.internal:host-gateway" volumes: @@ -61,6 +63,7 @@ services: - N8N_RUNNERS_AUTO_SHUTDOWN_TIMEOUT=300 depends_on: - n8n + networks: [cataclysm] volumes: - n8n-data:/home/node/.n8n volumes: diff --git a/pmoves/docs/NEXT_STEPS.md b/pmoves/docs/NEXT_STEPS.md index 2b3f086df3..aa86086233 100644 --- a/pmoves/docs/NEXT_STEPS.md +++ b/pmoves/docs/NEXT_STEPS.md @@ -12,6 +12,14 @@ _Last updated: 2026-03-12_ - Added `pmoves/docs/PMOVES.AI PLANS/CREATOR_NETWORK_CONTROL_PLANE.md` to frame YouTube, Discord agents, transcribe-and-fetch, model routing, and Tokenism as one creator-network lane. - Next focus: finish review cleanup, get PMOVES.YT/root checks green, and keep the creator/channel-monitor runbooks aligned with the authoritative runtime instead of the compatibility mirror. +### Latest changes (Mar 12, 2026) — n8n Production Control Plane Refresh +- `PMOVES-n8n` is now the authoritative n8n runtime/workflow lane; root `pmoves` consumes it instead of treating `pmoves/n8n/flows` as canon. +- n8n defaults to the dedicated `n8n-db` Postgres sidecar (`make -C pmoves up-n8n`), with SQLite reduced to a legacy escape hatch only. +- Added `make -C pmoves n8n-api-bootstrap` to automate owner bootstrap + Public API key rotation for n8n 2.1. +- Workflow activation/import now targets the n8n Public API path, replacing the failing CLI publish/unpublish fallback for the production lane. +- Added Supabase tracking schema `pmoves_core.n8n_workflow_registry` plus `make -C pmoves n8n-sync-supabase-registry` so PMOVES can inventory live workflow state. +- Next focus: validate the full bootstrap against Postgres-backed n8n, refresh PMOVES.YT from demo to production against the same automation lane, and decide which BotZ/MCP workflows join the shared canonical catalog. + ### Latest changes (Mar 8, 2026) — CI Runner Migration + RG-3 Automation - **AB-9 mitigation:** Migrated 10 lightweight CI jobs from `[self-hosted, Linux, X64]` to `ubuntu-latest`: - `sql-policy-lint`, `python-tests`, `webhook-smoke`, `yt-dlp-bump`, `deploy-gateway-agent` (validate only) diff --git a/pmoves/docs/PMOVES.AI PLANS/MAKE_TARGETS.md b/pmoves/docs/PMOVES.AI PLANS/MAKE_TARGETS.md index e5171ca5dc..dc67dc4227 100644 --- a/pmoves/docs/PMOVES.AI PLANS/MAKE_TARGETS.md +++ b/pmoves/docs/PMOVES.AI PLANS/MAKE_TARGETS.md @@ -72,6 +72,19 @@ Set `EXTERNAL_NEO4J|MEILI|QDRANT|SUPABASE=true` in `.env.local` to skip local in - `make up-nats` - Starts the NATS broker (`agents` profile) and rewrites `.env.local` so `YT_NATS_ENABLE=true` with `NATS_URL=nats://nats:pmoves@nats:4222`. - Use this before opting into the agents profile (Agent Zero, Archon, mesh-agent, Discord publisher). +- `make up-n8n` + - Starts the production/default n8n stack: `n8n`, `n8n-runners`, and the dedicated `n8n-db` Postgres sidecar. + - Canonical workflows come from `PMOVES-n8n/workflows`; `pmoves/n8n/flows` is only a compatibility mirror. +- `make n8n-api-bootstrap` + - Creates or logs into the n8n owner account, rotates the bootstrap Public API key, validates it, and writes `N8N_API_KEY` plus owner credentials into `pmoves/.env.local`. +- `make n8n-import-flows` + - Upserts the canonical `PMOVES-n8n` workflow catalog into the live n8n instance. +- `make n8n-activate-flows` + - Activates the default workflow set through the n8n Public API; keeps chat-platform voice flows inactive unless `VOICE_PLATFORMS=1`. +- `make n8n-sync-supabase-registry` + - Mirrors live workflow state into `pmoves_core.n8n_workflow_registry` so PMOVES can track n8n activity in Supabase. +- `make n8n-bootstrap` + - Full production bootstrap: `up-n8n -> n8n-api-bootstrap -> n8n-import-flows -> n8n-activate-flows -> n8n-sync-supabase-registry`. - `make mindmap-notebook-sync` - Runs `python pmoves/scripts/mindmap_to_notebook.py` to pull `/mindmap/{constellation_id}` entries out of `hi-rag-gateway-v2` and mirror them into Open Notebook via `/api/sources/json`. Requires `MINDMAP_BASE`, `MINDMAP_CONSTELLATION_ID`, `MINDMAP_NOTEBOOK_ID`, and `OPEN_NOTEBOOK_API_TOKEN`. - `make hirag-notebook-sync` diff --git a/pmoves/docs/PMOVES.AI PLANS/N8N_SETUP.md b/pmoves/docs/PMOVES.AI PLANS/N8N_SETUP.md index 79a8e018c1..83bb80a538 100644 --- a/pmoves/docs/PMOVES.AI PLANS/N8N_SETUP.md +++ b/pmoves/docs/PMOVES.AI PLANS/N8N_SETUP.md @@ -1,8 +1,8 @@ # n8n Setup Checklist (Supabase → Agent Zero → Discord) -_Last updated: 2025-12-14_ +_Last updated: 2026-03-12_ ## Overview -This guide streamlines importing and running the PMOVES approval and publish workflows in n8n. It targets Supabase CLI on the host, Agent Zero + NATS in Docker, and Discord webhooks. +This guide covers the production n8n path for PMOVES.AI. `PMOVES-n8n` is the authoritative runtime/workflow fork, n8n internals live on the dedicated `n8n-db` Postgres sidecar, and Supabase tracks PMOVES workflow state in `pmoves_core.n8n_workflow_registry`. ## Preflight (quick) - Start stacks: `make up && make up-agents && make up-n8n` @@ -14,8 +14,8 @@ This guide streamlines importing and running the PMOVES approval and publish wor - Supabase CLI running locally: `supabase start` or `make supa-start` - PMOVES stack up: `make up && make up-agents` - n8n running: - - Local/dev (SQLite): `make up-n8n` (UI at `http://localhost:5678`, launches `n8n` + `n8n-runners`) - - VPS/prod (Postgres): `N8N_DB=postgres make up-n8n` (adds `n8n-db` Postgres for durable state) + - Production/default: `make -C pmoves up-n8n` (starts `n8n`, `n8n-runners`, and `n8n-db`) + - Legacy escape hatch only: `N8N_DB=sqlite make -C pmoves up-n8n` - Secrets at hand: `SUPABASE_SERVICE_ROLE_KEY`, `DISCORD_WEBHOOK_URL`, `N8N_RUNNERS_AUTH_TOKEN` ## Environment (n8n) @@ -28,40 +28,50 @@ Set these in n8n (Settings → Variables) or via container env: - `DISCORD_WEBHOOK_USERNAME` = `PMOVES Publisher` - `N8N_RUNNERS_AUTH_TOKEN` = `` - `N8N_DEFAULT_TIMEZONE` = `America/New_York` (aligns cron schedules with project TZ) +- `N8N_DB_NAME` / `N8N_DB_USER` / `N8N_DB_PASSWORD` = dedicated `n8n-db` credentials +- `N8N_OWNER_EMAIL` / `N8N_OWNER_PASSWORD` = owner bootstrap credentials used by `make -C pmoves n8n-api-bootstrap` +- `N8N_API_KEY` = Public API key minted by `make -C pmoves n8n-api-bootstrap` ### n8n persistence mode (SQLite vs Postgres) -By default, `make up-n8n` runs n8n with SQLite for quick local bring-up. +By default, `make -C pmoves up-n8n` runs n8n on the dedicated Postgres sidecar. -For VPS/production, run n8n on Postgres: -- Set `N8N_DB=postgres` -- Set `N8N_DB_NAME`, `N8N_DB_USER`, `N8N_DB_PASSWORD` (in `pmoves/env.shared` or injected secrets) -- Bring up: `N8N_DB=postgres make -C pmoves up-n8n` +For production, keep n8n on Postgres: +- `N8N_DB=postgres` +- `N8N_DB_NAME`, `N8N_DB_USER`, `N8N_DB_PASSWORD` +- `make -C pmoves up-n8n` This uses `pmoves/docker-compose.n8n.postgres.yml` to add a dedicated `n8n-db` container. It does **not** move your PMOVES app data into n8n — it only stores n8n’s own workflow/execution state in Postgres. +Supabase remains the PMOVES system of record for workflow tracking, approvals, publishing, and operator UI state. + > **Supabase runtime note:** The CLI runtime binds REST on port `65421` per `supabase/config.toml`. If you switch back to the docker-compose PostgREST service, update `SUPABASE_REST_URL` accordingly (typically `http://host.docker.internal:54321/rest/v1`). Tip: These defaults are prewired in `docker-compose.n8n.yml`. If you use `make up-n8n`, populate `SUPABASE_SERVICE_ROLE_KEY`, `DISCORD_WEBHOOK_URL`, and `N8N_RUNNERS_AUTH_TOKEN` in `pmoves/env.shared` or `pmoves/.env.local`. The runner token is loaded via `env_file` (not compose-time interpolation) so recreating containers won’t accidentally desync the broker and sidecar. Note: n8n 1.115.3 already executes cron triggers in the main process. Avoid re-adding the deprecated `EXECUTIONS_PROCESS` flag—the service emits a warning and ignores it. +## Production Bootstrap +- `make -C pmoves up-n8n` +- `make -C pmoves n8n-api-bootstrap` +- `make -C pmoves n8n-import-flows` +- `make -C pmoves n8n-activate-flows` +- `make -C pmoves n8n-sync-supabase-registry` +- `make -C pmoves n8n-bootstrap` + +`n8n-api-bootstrap` creates or logs into the n8n owner account, rotates the `PMOVES.AI automation bootstrap` Public API key, validates it against `/api/v1/workflows`, and writes the resulting `N8N_API_KEY` plus owner credentials into `pmoves/.env.local`. + ## Container Tooling - The custom image defined in `compose/n8n/Dockerfile` bakes in the `sqlite3` CLI so DB inspections persist across restarts. - Run `make up-n8n` after pulling updates to rebuild the service when the Dockerfile changes. ## Import Workflows -1. Open n8n → Workflows → Import from File. -2. Import the core approvals stack: - - `pmoves/n8n/flows/approval_poller.json` - - `pmoves/n8n/flows/echo_publisher.json` -3. Import the creative webhooks (requires ComfyUI hosts prepared via [`pmoves/creator/README.md`](../creator/README.md)): - - `pmoves/n8n/flows/wan_to_cgp.webhook.json` - - `pmoves/n8n/flows/qwen_to_cgp.webhook.json` - - `pmoves/n8n/flows/vibevoice_to_cgp.webhook.json` -4. (Optional) Import the audio enrichment flows: - - `pmoves/n8n/flows/vibevoice_audio_ingest.json` - - `pmoves/n8n/flows/vibevoice_discord_preview.json` -5. Keep everything inactive until env is confirmed (Supabase keys, MinIO buckets, Discord webhooks). +Canonical workflow JSON now lives in `PMOVES-n8n/workflows/`. + +- Recommended: `make -C pmoves n8n-import-flows` +- Activation: `make -C pmoves n8n-activate-flows` +- Full bootstrap: `make -C pmoves n8n-bootstrap` + +The root `pmoves/n8n/flows/` directory is now only a compatibility mirror. ## Validate Env Bindings - Approval Poller @@ -85,6 +95,8 @@ Note: n8n 1.115.3 already executes cron triggers in the main process. Avoid re-a 5. Activate echo publisher → confirm Discord embed (title/link/thumbnail if provided) 6. Optional: Post directly to n8n webhook (flow must be active) - `make n8n-webhook-demo` +7. Sync the live workflow registry back into Supabase: + - `make -C pmoves n8n-sync-supabase-registry` ### Voice platform flows (Discord/Telegram) The repo includes optional chat-platform voice agent flows (Discord/Telegram) that require additional credentials and/or custom n8n nodes. @@ -126,6 +138,8 @@ These flows extend the core approval automations so we can surface RVC voice out ## Troubleshooting - 404 from Supabase in n8n: ensure `/rest/v1` is included in `SUPABASE_REST_URL`. +- `401` from `/api/v1/workflows`: run `make -C pmoves n8n-api-bootstrap` to rotate the Public API key. +- Workflow import works but activation fails: use the Public API path (`n8n-api-bootstrap` + `n8n-activate-flows`) instead of the legacy CLI publish fallback. - 503 from Agent Zero: confirm NATS + Agent Zero are running (`make up-agents`). - Discord no messages: verify `DISCORD_WEBHOOK_URL` and check rate limits in n8n logs. - n8n cannot reach host services on Linux: replace `host.docker.internal` with the host IP or Docker gateway (`172.17.0.1`). diff --git a/pmoves/docs/PMOVES.AI PLANS/ROADMAP.md b/pmoves/docs/PMOVES.AI PLANS/ROADMAP.md index 38e15d5cd3..6b2f426cbd 100644 --- a/pmoves/docs/PMOVES.AI PLANS/ROADMAP.md +++ b/pmoves/docs/PMOVES.AI PLANS/ROADMAP.md @@ -12,6 +12,12 @@ A production-ready, self-hostable orchestration mesh for creative + agent worklo - the Supabase docs sync contract was refreshed for the current CLI stack: `pmoves_core.tool_docs` writes now use schema-profile headers plus URL-encoded `on_conflict`. - root `pmoves/services/pmoves-yt` remains as a compatibility shim so existing tests/import paths keep working while production moves to the submodule. - downloader defaults are being normalized around the authoritative runtime: PMOVES.YT now documents the modern client/token path, root compose passes explicit companion wiring, and Jellyfin/channel-monitor docs are being moved off older MCP and future-work framing. +- March 12 n8n production-path remediation landed locally: + - `PMOVES-n8n` is now the authoritative runtime/workflow lane consumed by the root repo. + - `make -C pmoves up-n8n` now defaults to the dedicated `n8n-db` Postgres sidecar instead of SQLite. + - n8n owner/bootstrap automation is scripted (`n8n-api-bootstrap`) so Public API keys no longer depend on manual UI steps. + - workflow activation now targets the n8n 2.1 Public API (`/api/v1/workflows/{id}/activate|deactivate`) instead of the brittle CLI publish fallback. + - Supabase tracking contract added: `pmoves_core.n8n_workflow_registry` stores live workflow state synced from n8n. - March 7 merge wave completed on `main`: `#814`, `#815`, `#816`, `#817`, `#818`, `#819`, `#820`, `#821` (8 PRs, 3 batches). - Chrome extension security hardening landed in `#821`: 9 CodeRabbit review items addressed (auth storage isolation, XSS remediation, mock server hardening, timeout guards, state management fixes, CSP). - Distributed topology documentation + examples landed in `#820`. diff --git a/pmoves/env.shared.example b/pmoves/env.shared.example index 7a1f12f61c..e4ccab796d 100644 --- a/pmoves/env.shared.example +++ b/pmoves/env.shared.example @@ -100,6 +100,21 @@ NEXT_PUBLIC_SUPABASE_BOOT_USER_JWT= AUTH_BOOTSTRAP_MODE=jwt AUTH_BOOTSTRAP_STRICT=0 +# n8n production defaults +# Keep n8n internals on the dedicated sidecar Postgres. PMOVES domain state still lives in Supabase. +N8N_DB=postgres +N8N_DB_NAME=n8n +N8N_DB_USER=n8n +N8N_DB_PASSWORD=CHANGE_ME_N8N_DB_PASSWORD +N8N_DB_SCHEMA=public +N8N_BASE_URL=http://localhost:5678 +N8N_API_URL=${N8N_BASE_URL}/api/v1 +N8N_OWNER_EMAIL=${SUPABASE_BOOT_USER_EMAIL} +N8N_OWNER_PASSWORD= +N8N_OWNER_FIRST_NAME=PMOVES +N8N_OWNER_LAST_NAME=Operator +N8N_API_KEY= + # Optional Google OAuth (Supabase Auth) SUPABASE_AUTH_EXTERNAL_GOOGLE_ENABLED=false SUPABASE_AUTH_EXTERNAL_GOOGLE_CLIENT_ID= diff --git a/pmoves/n8n/README.md b/pmoves/n8n/README.md index f0584d6efd..6173eb3431 100644 --- a/pmoves/n8n/README.md +++ b/pmoves/n8n/README.md @@ -7,7 +7,9 @@ Canonical n8n workflow ownership lives in [`PMOVES-n8n/workflows/`](../../PMOVES Canonical edit path: ```bash +make -C pmoves n8n-api-bootstrap make -C pmoves n8n-sync-submodule-flows make -C pmoves n8n-import-flows make -C pmoves n8n-activate-flows +make -C pmoves n8n-sync-supabase-registry ``` diff --git a/pmoves/supabase/migrations/20260312130000_n8n_workflow_registry.sql b/pmoves/supabase/migrations/20260312130000_n8n_workflow_registry.sql new file mode 100644 index 0000000000..f707a42623 --- /dev/null +++ b/pmoves/supabase/migrations/20260312130000_n8n_workflow_registry.sql @@ -0,0 +1,49 @@ +create schema if not exists pmoves_core; + +create table if not exists pmoves_core.n8n_workflow_registry ( + workflow_id text primary key, + workflow_name text not null, + canonical_filename text, + canonical_path text, + source_repo text not null default 'PMOVES-n8n', + source_submodule_path text not null default 'PMOVES-n8n/workflows', + target_active boolean not null default false, + is_active boolean not null default false, + version_id uuid, + active_version_id uuid, + project_id text, + nodes_count integer not null default 0, + tags jsonb not null default '[]'::jsonb, + sync_meta jsonb not null default '{}'::jsonb, + last_synced_at timestamptz not null default timezone('utc', now()) +); + +create index if not exists n8n_workflow_registry_is_active_idx + on pmoves_core.n8n_workflow_registry (is_active); + +create index if not exists n8n_workflow_registry_target_active_idx + on pmoves_core.n8n_workflow_registry (target_active); + +grant usage on schema pmoves_core to authenticated, service_role; +grant select on pmoves_core.n8n_workflow_registry to authenticated; +grant select, insert, update, delete on pmoves_core.n8n_workflow_registry to service_role; + +alter table pmoves_core.n8n_workflow_registry enable row level security; + +drop policy if exists "n8n workflow registry read" on pmoves_core.n8n_workflow_registry; +create policy "n8n workflow registry read" + on pmoves_core.n8n_workflow_registry + for select + to authenticated + using (true); + +drop policy if exists "n8n workflow registry service write" on pmoves_core.n8n_workflow_registry; +create policy "n8n workflow registry service write" + on pmoves_core.n8n_workflow_registry + for all + to service_role + using (true) + with check (true); + +comment on table pmoves_core.n8n_workflow_registry is + 'Live n8n workflow inventory synced from PMOVES-n8n via the n8n Public API.'; diff --git a/pmoves/tools/brand_defaults.py b/pmoves/tools/brand_defaults.py index 916db40ce7..d970663166 100644 --- a/pmoves/tools/brand_defaults.py +++ b/pmoves/tools/brand_defaults.py @@ -124,7 +124,8 @@ def _ensure_integration_credentials(text: str) -> str: text = _set_kv(text, "FIREFLY_APP_KEY", firefly_key) # n8n encryption key: 32-byte urlsafe token for workflow credential encryption. - # NOTE: N8N_API_KEY cannot be auto-generated — it must be created from the n8n UI. + # N8N_API_KEY is still not generated here because it requires a live n8n + # instance; use `make -C pmoves n8n-api-bootstrap` after bring-up. n8n_enc = _get_kv(text, "N8N_ENCRYPTION_KEY") if _is_blank_or_placeholder(n8n_enc): text = _set_kv(text, "N8N_ENCRYPTION_KEY", _strong_random(32)) From 699e6fda3e1ea632ea3e6e58e510c9c79dbafa79 Mon Sep 17 00:00:00 2001 From: Shaela Bello Date: Thu, 12 Mar 2026 12:54:57 -0400 Subject: [PATCH 3/3] fix(sql): replace blanket USING(true) with explicit role checks in n8n registry SQL Policy Lint rejects USING(true) as an unsafe blanket policy. Replace with auth.role() checks that are functionally equivalent but pass the linter. Co-Authored-By: Claude Opus 4.6 --- .../migrations/20260312130000_n8n_workflow_registry.sql | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pmoves/supabase/migrations/20260312130000_n8n_workflow_registry.sql b/pmoves/supabase/migrations/20260312130000_n8n_workflow_registry.sql index f707a42623..d2d5944c51 100644 --- a/pmoves/supabase/migrations/20260312130000_n8n_workflow_registry.sql +++ b/pmoves/supabase/migrations/20260312130000_n8n_workflow_registry.sql @@ -35,15 +35,15 @@ create policy "n8n workflow registry read" on pmoves_core.n8n_workflow_registry for select to authenticated - using (true); + using (auth.role() = 'authenticated'); drop policy if exists "n8n workflow registry service write" on pmoves_core.n8n_workflow_registry; create policy "n8n workflow registry service write" on pmoves_core.n8n_workflow_registry for all to service_role - using (true) - with check (true); + using (auth.role() = 'service_role') + with check (auth.role() = 'service_role'); comment on table pmoves_core.n8n_workflow_registry is 'Live n8n workflow inventory synced from PMOVES-n8n via the n8n Public API.';