diff --git a/data/theme-backups/theme-2026-04-02T09-33-40-460Z.json b/data/theme-backups/theme-2026-04-02T09-33-40-460Z.json new file mode 100644 index 0000000..7aaad8c --- /dev/null +++ b/data/theme-backups/theme-2026-04-02T09-33-40-460Z.json @@ -0,0 +1,50 @@ +{ + "brand": { + "name": "Helix Engage", + "hospitalName": "Global Hospital", + "logo": "/helix-logo.png", + "favicon": "/favicon.ico" + }, + "colors": { + "brand": { + "25": "rgb(239 246 255)", + "50": "rgb(219 234 254)", + "100": "rgb(191 219 254)", + "200": "rgb(147 197 253)", + "300": "rgb(96 165 250)", + "400": "rgb(59 130 246)", + "500": "rgb(37 99 235)", + "600": "rgb(29 78 216)", + "700": "rgb(30 64 175)", + "800": "rgb(30 58 138)", + "900": "rgb(23 37 84)", + "950": "rgb(15 23 42)" + } + }, + "typography": { + "body": "'Satoshi', var(--font-inter, 'Inter'), -apple-system, 'Segoe UI', Roboto, Arial, sans-serif", + "display": "'General Sans', var(--font-inter, 'Inter'), -apple-system, 'Segoe UI', Roboto, Arial, sans-serif" + }, + "login": { + "title": "Sign in to Helix Engage", + "subtitle": "Global Hospital", + "showGoogleSignIn": true, + "showForgotPassword": true, + "poweredBy": { + "label": "Powered by F0rty2.ai", + "url": "https://f0rty2.ai" + } + }, + "sidebar": { + "title": "Helix Engage", + "subtitle": "Global Hospital · {role}" + }, + "ai": { + "quickActions": [ + { "label": "Doctor availability", "prompt": "What doctors are available and what are their visiting hours?" }, + { "label": "Clinic timings", "prompt": "What are the clinic locations and timings?" }, + { "label": "Patient history", "prompt": "Can you summarize this patient's history?" }, + { "label": "Treatment packages", "prompt": "What treatment packages are available?" } + ] + } +} diff --git a/data/theme-backups/theme-2026-04-02T09-34-04-404Z.json b/data/theme-backups/theme-2026-04-02T09-34-04-404Z.json new file mode 100644 index 0000000..24033fa --- /dev/null +++ b/data/theme-backups/theme-2026-04-02T09-34-04-404Z.json @@ -0,0 +1,62 @@ +{ + "brand": { + "name": "Test", + "hospitalName": "Global Hospital", + "logo": "/helix-logo.png", + "favicon": "/favicon.ico" + }, + "colors": { + "brand": { + "25": "rgb(239 246 255)", + "50": "rgb(219 234 254)", + "100": "rgb(191 219 254)", + "200": "rgb(147 197 253)", + "300": "rgb(96 165 250)", + "400": "rgb(59 130 246)", + "500": "rgb(37 99 235)", + "600": "rgb(29 78 216)", + "700": "rgb(30 64 175)", + "800": "rgb(30 58 138)", + "900": "rgb(23 37 84)", + "950": "rgb(15 23 42)" + } + }, + "typography": { + "body": "'Satoshi', var(--font-inter, 'Inter'), -apple-system, 'Segoe UI', Roboto, Arial, sans-serif", + "display": "'General Sans', var(--font-inter, 'Inter'), -apple-system, 'Segoe UI', Roboto, Arial, sans-serif" + }, + "login": { + "title": "Sign in to Helix Engage", + "subtitle": "Global Hospital", + "showGoogleSignIn": true, + "showForgotPassword": true, + "poweredBy": { + "label": "Powered by F0rty2.ai", + "url": "https://f0rty2.ai" + } + }, + "sidebar": { + "title": "Helix Engage", + "subtitle": "Global Hospital · {role}" + }, + "ai": { + "quickActions": [ + { + "label": "Doctor availability", + "prompt": "What doctors are available and what are their visiting hours?" + }, + { + "label": "Clinic timings", + "prompt": "What are the clinic locations and timings?" + }, + { + "label": "Patient history", + "prompt": "Can you summarize this patient's history?" + }, + { + "label": "Treatment packages", + "prompt": "What treatment packages are available?" + } + ] + } +} \ No newline at end of file diff --git a/data/theme-backups/theme-2026-04-02T09-41-45-744Z.json b/data/theme-backups/theme-2026-04-02T09-41-45-744Z.json new file mode 100644 index 0000000..797bce7 --- /dev/null +++ b/data/theme-backups/theme-2026-04-02T09-41-45-744Z.json @@ -0,0 +1,62 @@ +{ + "brand": { + "name": "Helix Engage", + "hospitalName": "Global Hospital", + "logo": "/helix-logo.png", + "favicon": "/favicon.ico" + }, + "colors": { + "brand": { + "25": "rgb(239 246 255)", + "50": "rgb(219 234 254)", + "100": "rgb(191 219 254)", + "200": "rgb(147 197 253)", + "300": "rgb(96 165 250)", + "400": "rgb(59 130 246)", + "500": "rgb(37 99 235)", + "600": "rgb(29 78 216)", + "700": "rgb(30 64 175)", + "800": "rgb(30 58 138)", + "900": "rgb(23 37 84)", + "950": "rgb(15 23 42)" + } + }, + "typography": { + "body": "'Satoshi', var(--font-inter, 'Inter'), -apple-system, 'Segoe UI', Roboto, Arial, sans-serif", + "display": "'General Sans', var(--font-inter, 'Inter'), -apple-system, 'Segoe UI', Roboto, Arial, sans-serif" + }, + "login": { + "title": "Sign in to Helix Engage", + "subtitle": "Global Hospital", + "showGoogleSignIn": true, + "showForgotPassword": true, + "poweredBy": { + "label": "Powered by F0rty2.ai", + "url": "https://f0rty2.ai" + } + }, + "sidebar": { + "title": "Helix Engage", + "subtitle": "Global Hospital · {role}" + }, + "ai": { + "quickActions": [ + { + "label": "Doctor availability", + "prompt": "What doctors are available and what are their visiting hours?" + }, + { + "label": "Clinic timings", + "prompt": "What are the clinic locations and timings?" + }, + { + "label": "Patient history", + "prompt": "Can you summarize this patient's history?" + }, + { + "label": "Treatment packages", + "prompt": "What treatment packages are available?" + } + ] + } +} \ No newline at end of file diff --git a/data/theme-backups/theme-2026-04-02T09-42-24-047Z.json b/data/theme-backups/theme-2026-04-02T09-42-24-047Z.json new file mode 100644 index 0000000..9b43cdb --- /dev/null +++ b/data/theme-backups/theme-2026-04-02T09-42-24-047Z.json @@ -0,0 +1,62 @@ +{ + "brand": { + "name": "Helix Engage", + "hospitalName": "Global Hospital", + "logo": "/helix-logo.png", + "favicon": "/favicon.ico" + }, + "colors": { + "brand": { + "25": "rgb(250 245 255)", + "50": "rgb(245 235 255)", + "100": "rgb(235 215 254)", + "200": "rgb(214 187 251)", + "300": "rgb(182 146 246)", + "400": "rgb(158 119 237)", + "500": "rgb(127 86 217)", + "600": "rgb(105 65 198)", + "700": "rgb(83 56 158)", + "800": "rgb(66 48 125)", + "900": "rgb(53 40 100)", + "950": "rgb(44 28 95)" + } + }, + "typography": { + "body": "'Satoshi', var(--font-inter, 'Inter'), -apple-system, 'Segoe UI', Roboto, Arial, sans-serif", + "display": "'General Sans', var(--font-inter, 'Inter'), -apple-system, 'Segoe UI', Roboto, Arial, sans-serif" + }, + "login": { + "title": "Sign in to Ramaiah", + "subtitle": "Ramaiah Hospital", + "showGoogleSignIn": true, + "showForgotPassword": true, + "poweredBy": { + "label": "Powered by F0rty2.ai", + "url": "https://f0rty2.ai" + } + }, + "sidebar": { + "title": "Ramaiah", + "subtitle": "Ramaiah Hospital · {role}" + }, + "ai": { + "quickActions": [ + { + "label": "Doctor availability", + "prompt": "What doctors are available and what are their visiting hours?" + }, + { + "label": "Clinic timings", + "prompt": "What are the clinic locations and timings?" + }, + { + "label": "Patient history", + "prompt": "Can you summarize this patient's history?" + }, + { + "label": "Treatment packages", + "prompt": "What treatment packages are available?" + } + ] + } +} \ No newline at end of file diff --git a/data/theme-backups/theme-2026-04-02T09-43-19-186Z.json b/data/theme-backups/theme-2026-04-02T09-43-19-186Z.json new file mode 100644 index 0000000..ded5789 --- /dev/null +++ b/data/theme-backups/theme-2026-04-02T09-43-19-186Z.json @@ -0,0 +1,62 @@ +{ + "brand": { + "name": "Helix Engage", + "hospitalName": "Global Hospital", + "logo": "/helix-logo.png", + "favicon": "/favicon.ico" + }, + "colors": { + "brand": { + "25": "rgb(248 250 252)", + "50": "rgb(241 245 249)", + "100": "rgb(226 232 240)", + "200": "rgb(203 213 225)", + "300": "rgb(148 163 184)", + "400": "rgb(100 116 139)", + "500": "rgb(71 85 105)", + "600": "rgb(47 64 89)", + "700": "rgb(37 49 72)", + "800": "rgb(30 41 59)", + "900": "rgb(15 23 42)", + "950": "rgb(2 6 23)" + } + }, + "typography": { + "body": "'Plus Jakarta Sans', 'Inter', sans-serif", + "display": "'Plus Jakarta Sans', 'Inter', sans-serif" + }, + "login": { + "title": "Sign in to Ramaiah", + "subtitle": "Ramaiah Hospital", + "showGoogleSignIn": true, + "showForgotPassword": true, + "poweredBy": { + "label": "Powered by F0rty2.ai", + "url": "https://f0rty2.ai" + } + }, + "sidebar": { + "title": "Ramaiah", + "subtitle": "Ramaiah Hospital · {role}" + }, + "ai": { + "quickActions": [ + { + "label": "Doctor availability", + "prompt": "What doctors are available and what are their visiting hours?" + }, + { + "label": "Clinic timings", + "prompt": "What are the clinic locations and timings?" + }, + { + "label": "Patient history", + "prompt": "Can you summarize this patient's history?" + }, + { + "label": "Treatment packages", + "prompt": "What treatment packages are available?" + } + ] + } +} \ No newline at end of file diff --git a/data/theme-backups/theme-2026-04-02T09-53-00-903Z.json b/data/theme-backups/theme-2026-04-02T09-53-00-903Z.json new file mode 100644 index 0000000..4e522f9 --- /dev/null +++ b/data/theme-backups/theme-2026-04-02T09-53-00-903Z.json @@ -0,0 +1,62 @@ +{ + "brand": { + "name": "Helix Engage", + "hospitalName": "Global Hospital", + "logo": "/helix-logo.png", + "favicon": "/favicon.ico" + }, + "colors": { + "brand": { + "25": "rgb(248 250 252)", + "50": "rgb(241 245 249)", + "100": "rgb(226 232 240)", + "200": "rgb(203 213 225)", + "300": "rgb(148 163 184)", + "400": "rgb(100 116 139)", + "500": "rgb(71 85 105)", + "600": "rgb(47 64 89)", + "700": "rgb(37 49 72)", + "800": "rgb(30 41 59)", + "900": "rgb(15 23 42)", + "950": "rgb(2 6 23)" + } + }, + "typography": { + "body": "'Plus Jakarta Sans', 'Inter', sans-serif", + "display": "'Plus Jakarta Sans', 'Inter', sans-serif" + }, + "login": { + "title": "Sign in to Ramaiah", + "subtitle": "Ramaiah Hospital", + "showGoogleSignIn": false, + "showForgotPassword": true, + "poweredBy": { + "label": "Powered by F0rty2.ai", + "url": "https://f0rty2.ai" + } + }, + "sidebar": { + "title": "Ramaiah", + "subtitle": "Ramaiah Hospital · {role}" + }, + "ai": { + "quickActions": [ + { + "label": "Doctor availability", + "prompt": "What doctors are available and what are their visiting hours?" + }, + { + "label": "Clinic timings", + "prompt": "What are the clinic locations and timings?" + }, + { + "label": "Patient history", + "prompt": "Can you summarize this patient's history?" + }, + { + "label": "Treatment packages", + "prompt": "What treatment packages are available?" + } + ] + } +} \ No newline at end of file diff --git a/data/theme-backups/theme-2026-04-02T10-00-48-735Z.json b/data/theme-backups/theme-2026-04-02T10-00-48-735Z.json new file mode 100644 index 0000000..05d1ffd --- /dev/null +++ b/data/theme-backups/theme-2026-04-02T10-00-48-735Z.json @@ -0,0 +1,62 @@ +{ + "brand": { + "name": "Helix Engage", + "hospitalName": "Global Hospital", + "logo": "/helix-logo.png", + "favicon": "/favicon.ico" + }, + "colors": { + "brand": { + "25": "rgb(240 253 250)", + "50": "rgb(204 251 241)", + "100": "rgb(153 246 228)", + "200": "rgb(94 234 212)", + "300": "rgb(45 212 191)", + "400": "rgb(20 184 166)", + "500": "rgb(13 148 136)", + "600": "rgb(15 118 110)", + "700": "rgb(17 94 89)", + "800": "rgb(19 78 74)", + "900": "rgb(17 63 61)", + "950": "rgb(4 47 46)" + } + }, + "typography": { + "body": "'Plus Jakarta Sans', 'Inter', sans-serif", + "display": "'Plus Jakarta Sans', 'Inter', sans-serif" + }, + "login": { + "title": "Sign in to Ramaiah", + "subtitle": "Ramaiah Hospital", + "showGoogleSignIn": false, + "showForgotPassword": true, + "poweredBy": { + "label": "Powered by F0rty2.ai", + "url": "https://f0rty2.ai" + } + }, + "sidebar": { + "title": "Ramaiah", + "subtitle": "Ramaiah Hospital · {role}" + }, + "ai": { + "quickActions": [ + { + "label": "Doctor availability", + "prompt": "What doctors are available and what are their visiting hours?" + }, + { + "label": "Clinic timings", + "prompt": "What are the clinic locations and timings?" + }, + { + "label": "Patient history", + "prompt": "Can you summarize this patient's history?" + }, + { + "label": "Treatment packages", + "prompt": "What treatment packages are available?" + } + ] + } +} \ No newline at end of file diff --git a/data/theme-backups/theme-2026-04-02T10-19-29-559Z.json b/data/theme-backups/theme-2026-04-02T10-19-29-559Z.json new file mode 100644 index 0000000..da764b4 --- /dev/null +++ b/data/theme-backups/theme-2026-04-02T10-19-29-559Z.json @@ -0,0 +1,62 @@ +{ + "brand": { + "name": "Helix Engage", + "hospitalName": "Global Hospital", + "logo": "/helix-logo.png", + "favicon": "/favicon.ico" + }, + "colors": { + "brand": { + "25": "rgb(240 253 250)", + "50": "rgb(204 251 241)", + "100": "rgb(153 246 228)", + "200": "rgb(94 234 212)", + "300": "rgb(45 212 191)", + "400": "rgb(20 184 166)", + "500": "rgb(13 148 136)", + "600": "rgb(15 118 110)", + "700": "rgb(17 94 89)", + "800": "rgb(19 78 74)", + "900": "rgb(17 63 61)", + "950": "rgb(4 47 46)" + } + }, + "typography": { + "body": "'Satoshi', 'Inter', -apple-system, sans-serif", + "display": "'Satoshi', 'Inter', -apple-system, sans-serif" + }, + "login": { + "title": "Sign in to Ramaiah", + "subtitle": "Ramaiah Hospital", + "showGoogleSignIn": false, + "showForgotPassword": true, + "poweredBy": { + "label": "Powered by F0rty2.ai", + "url": "https://f0rty2.ai" + } + }, + "sidebar": { + "title": "Ramaiah", + "subtitle": "Ramaiah Hospital · {role}" + }, + "ai": { + "quickActions": [ + { + "label": "Doctor availability", + "prompt": "What doctors are available and what are their visiting hours?" + }, + { + "label": "Clinic timings", + "prompt": "What are the clinic locations and timings?" + }, + { + "label": "Patient history", + "prompt": "Can you summarize this patient's history?" + }, + { + "label": "Treatment packages", + "prompt": "What treatment packages are available?" + } + ] + } +} \ No newline at end of file diff --git a/data/theme-backups/theme-2026-04-02T10-19-35-284Z.json b/data/theme-backups/theme-2026-04-02T10-19-35-284Z.json new file mode 100644 index 0000000..68e414b --- /dev/null +++ b/data/theme-backups/theme-2026-04-02T10-19-35-284Z.json @@ -0,0 +1,64 @@ +{ + "brand": { + "name": "Helix Engage", + "hospitalName": "Global Hospital", + "logo": "/helix-logo.png", + "favicon": "/favicon.ico" + }, + "colors": { + "brand": { + "25": "rgb(249 252 243)", + "50": "rgb(244 249 231)", + "100": "rgb(235 244 210)", + "200": "rgb(224 247 161)", + "300": "rgb(206 243 104)", + "400": "rgb(195 255 31)", + "500": "rgb(172 235 0)", + "600": "rgb(142 194 0)", + "700": "rgb(116 158 0)", + "800": "rgb(97 133 0)", + "900": "rgb(75 102 0)", + "950": "rgb(49 66 0)" + } + }, + "typography": { + "body": "'Satoshi', 'Inter', -apple-system, sans-serif", + "display": "'Satoshi', 'Inter', -apple-system, sans-serif" + }, + "login": { + "title": "Sign in to Ramaiah", + "subtitle": "Ramaiah Hospital", + "showGoogleSignIn": false, + "showForgotPassword": true, + "poweredBy": { + "label": "Powered by F0rty2.ai", + "url": "https://f0rty2.ai" + } + }, + "sidebar": { + "title": "Ramaiah", + "subtitle": "Ramaiah Hospital · {role}" + }, + "ai": { + "quickActions": [ + { + "label": "Doctor availability", + "prompt": "What doctors are available and what are their visiting hours?" + }, + { + "label": "Clinic timings", + "prompt": "What are the clinic locations and timings?" + }, + { + "label": "Patient history", + "prompt": "Can you summarize this patient's history?" + }, + { + "label": "Treatment packages", + "prompt": "What treatment packages are available?" + } + ] + }, + "version": 1, + "updatedAt": "2026-04-02T10:19:29.559Z" +} \ No newline at end of file diff --git a/data/theme.json b/data/theme.json new file mode 100644 index 0000000..cc44b03 --- /dev/null +++ b/data/theme.json @@ -0,0 +1,64 @@ +{ + "brand": { + "name": "Helix Engage", + "hospitalName": "Global Hospital", + "logo": "/helix-logo.png", + "favicon": "/favicon.ico" + }, + "colors": { + "brand": { + "25": "rgb(250 245 255)", + "50": "rgb(245 235 255)", + "100": "rgb(235 215 254)", + "200": "rgb(214 187 251)", + "300": "rgb(182 146 246)", + "400": "rgb(158 119 237)", + "500": "rgb(127 86 217)", + "600": "rgb(105 65 198)", + "700": "rgb(83 56 158)", + "800": "rgb(66 48 125)", + "900": "rgb(53 40 100)", + "950": "rgb(44 28 95)" + } + }, + "typography": { + "body": "'Satoshi', 'Inter', -apple-system, sans-serif", + "display": "'Satoshi', 'Inter', -apple-system, sans-serif" + }, + "login": { + "title": "Sign in to Ramaiah", + "subtitle": "Ramaiah Hospital", + "showGoogleSignIn": false, + "showForgotPassword": true, + "poweredBy": { + "label": "Powered by F0rty2.ai", + "url": "https://f0rty2.ai" + } + }, + "sidebar": { + "title": "Ramaiah", + "subtitle": "Ramaiah Hospital · {role}" + }, + "ai": { + "quickActions": [ + { + "label": "Doctor availability", + "prompt": "What doctors are available and what are their visiting hours?" + }, + { + "label": "Clinic timings", + "prompt": "What are the clinic locations and timings?" + }, + { + "label": "Patient history", + "prompt": "Can you summarize this patient's history?" + }, + { + "label": "Treatment packages", + "prompt": "What treatment packages are available?" + } + ] + }, + "version": 2, + "updatedAt": "2026-04-02T10:19:35.284Z" +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 05c3d87..10e6e87 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,9 @@ "@ai-sdk/anthropic": "^3.0.58", "@ai-sdk/openai": "^3.0.41", "@deepgram/sdk": "^5.0.0", + "@livekit/agents": "^1.2.1", + "@livekit/agents-plugin-google": "^1.2.1", + "@livekit/agents-plugin-silero": "^1.2.1", "@nestjs/common": "^11.0.1", "@nestjs/config": "^4.0.3", "@nestjs/core": "^11.0.1", @@ -21,9 +24,12 @@ "ai": "^6.0.116", "axios": "^1.13.6", "ioredis": "^5.10.1", + "json-rules-engine": "^6.6.0", + "kafkajs": "^2.2.4", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", - "socket.io": "^4.8.3" + "socket.io": "^4.8.3", + "zod": "^4.3.6" }, "devDependencies": { "@eslint/eslintrc": "^3.2.0", @@ -800,6 +806,12 @@ "url": "https://github.com/sponsors/Borewit" } }, + "node_modules/@bufbuild/protobuf": { + "version": "1.10.1", + "resolved": "http://localhost:4873/@bufbuild/protobuf/-/protobuf-1.10.1.tgz", + "integrity": "sha512-wJ8ReQbHxsAfXhrf9ixl0aYbZorRuOWpBNzm8pL8ftmSxQx/wnJD5Eg861NwJU/czy2VXFIebCeZnZrI9rktIQ==", + "license": "(Apache-2.0 AND BSD-3-Clause)" + }, "node_modules/@colors/colors": { "version": "1.5.0", "resolved": "http://localhost:4873/@colors/colors/-/colors-1.5.0.tgz", @@ -835,6 +847,13 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@datastructures-js/deque": { + "version": "1.0.8", + "resolved": "http://localhost:4873/@datastructures-js/deque/-/deque-1.0.8.tgz", + "integrity": "sha512-PSBhJ2/SmeRPRHuBv7i/fHWIdSC3JTyq56qb+Rq0wjOagi0/fdV5/B/3Md5zFZus/W6OkSPMaxMKKMNMrSmubg==", + "license": "MIT", + "peer": true + }, "node_modules/@deepgram/sdk": { "version": "5.0.0", "resolved": "http://localhost:4873/@deepgram/sdk/-/sdk-5.0.0.tgz", @@ -863,7 +882,6 @@ "version": "1.9.0", "resolved": "http://localhost:4873/@emnapi/runtime/-/runtime-1.9.0.tgz", "integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1480,6 +1498,155 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@ffmpeg-installer/darwin-arm64": { + "version": "4.1.5", + "resolved": "http://localhost:4873/@ffmpeg-installer/darwin-arm64/-/darwin-arm64-4.1.5.tgz", + "integrity": "sha512-hYqTiP63mXz7wSQfuqfFwfLOfwwFChUedeCVKkBtl/cliaTM7/ePI9bVzfZ2c+dWu3TqCwLDRWNSJ5pqZl8otA==", + "cpu": [ + "arm64" + ], + "hasInstallScript": true, + "license": "https://git.ffmpeg.org/gitweb/ffmpeg.git/blob_plain/HEAD:/LICENSE.md", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@ffmpeg-installer/darwin-x64": { + "version": "4.1.0", + "resolved": "http://localhost:4873/@ffmpeg-installer/darwin-x64/-/darwin-x64-4.1.0.tgz", + "integrity": "sha512-Z4EyG3cIFjdhlY8wI9aLUXuH8nVt7E9SlMVZtWvSPnm2sm37/yC2CwjUzyCQbJbySnef1tQwGG2Sx+uWhd9IAw==", + "cpu": [ + "x64" + ], + "hasInstallScript": true, + "license": "LGPL-2.1", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@ffmpeg-installer/ffmpeg": { + "version": "1.1.0", + "resolved": "http://localhost:4873/@ffmpeg-installer/ffmpeg/-/ffmpeg-1.1.0.tgz", + "integrity": "sha512-Uq4rmwkdGxIa9A6Bd/VqqYbT7zqh1GrT5/rFwCwKM70b42W5gIjWeVETq6SdcL0zXqDtY081Ws/iJWhr1+xvQg==", + "license": "LGPL-2.1", + "optionalDependencies": { + "@ffmpeg-installer/darwin-arm64": "4.1.5", + "@ffmpeg-installer/darwin-x64": "4.1.0", + "@ffmpeg-installer/linux-arm": "4.1.3", + "@ffmpeg-installer/linux-arm64": "4.1.4", + "@ffmpeg-installer/linux-ia32": "4.1.0", + "@ffmpeg-installer/linux-x64": "4.1.0", + "@ffmpeg-installer/win32-ia32": "4.1.0", + "@ffmpeg-installer/win32-x64": "4.1.0" + } + }, + "node_modules/@ffmpeg-installer/linux-arm": { + "version": "4.1.3", + "resolved": "http://localhost:4873/@ffmpeg-installer/linux-arm/-/linux-arm-4.1.3.tgz", + "integrity": "sha512-NDf5V6l8AfzZ8WzUGZ5mV8O/xMzRag2ETR6+TlGIsMHp81agx51cqpPItXPib/nAZYmo55Bl2L6/WOMI3A5YRg==", + "cpu": [ + "arm" + ], + "hasInstallScript": true, + "license": "GPLv3", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@ffmpeg-installer/linux-arm64": { + "version": "4.1.4", + "resolved": "http://localhost:4873/@ffmpeg-installer/linux-arm64/-/linux-arm64-4.1.4.tgz", + "integrity": "sha512-dljEqAOD0oIM6O6DxBW9US/FkvqvQwgJ2lGHOwHDDwu/pX8+V0YsDL1xqHbj1DMX/+nP9rxw7G7gcUvGspSoKg==", + "cpu": [ + "arm64" + ], + "hasInstallScript": true, + "license": "GPLv3", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@ffmpeg-installer/linux-ia32": { + "version": "4.1.0", + "resolved": "http://localhost:4873/@ffmpeg-installer/linux-ia32/-/linux-ia32-4.1.0.tgz", + "integrity": "sha512-0LWyFQnPf+Ij9GQGD034hS6A90URNu9HCtQ5cTqo5MxOEc7Rd8gLXrJvn++UmxhU0J5RyRE9KRYstdCVUjkNOQ==", + "cpu": [ + "ia32" + ], + "hasInstallScript": true, + "license": "GPLv3", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@ffmpeg-installer/linux-x64": { + "version": "4.1.0", + "resolved": "http://localhost:4873/@ffmpeg-installer/linux-x64/-/linux-x64-4.1.0.tgz", + "integrity": "sha512-Y5BWhGLU/WpQjOArNIgXD3z5mxxdV8c41C+U15nsE5yF8tVcdCGet5zPs5Zy3Ta6bU7haGpIzryutqCGQA/W8A==", + "cpu": [ + "x64" + ], + "hasInstallScript": true, + "license": "GPLv3", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@ffmpeg-installer/win32-ia32": { + "version": "4.1.0", + "resolved": "http://localhost:4873/@ffmpeg-installer/win32-ia32/-/win32-ia32-4.1.0.tgz", + "integrity": "sha512-FV2D7RlaZv/lrtdhaQ4oETwoFUsUjlUiasiZLDxhEUPdNDWcH1OU9K1xTvqz+OXLdsmYelUDuBS/zkMOTtlUAw==", + "cpu": [ + "ia32" + ], + "license": "GPLv3", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@ffmpeg-installer/win32-x64": { + "version": "4.1.0", + "resolved": "http://localhost:4873/@ffmpeg-installer/win32-x64/-/win32-x64-4.1.0.tgz", + "integrity": "sha512-Drt5u2vzDnIONf4ZEkKtFlbvwj6rI3kxw1Ck9fpudmtgaZIHD4ucsWB2lCZBXRxJgXR+2IMSti+4rtM4C4rXgg==", + "cpu": [ + "x64" + ], + "license": "GPLv3", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@google/genai": { + "version": "1.46.0", + "resolved": "http://localhost:4873/@google/genai/-/genai-1.46.0.tgz", + "integrity": "sha512-ewPMN5JkKfgU5/kdco9ZhXBHDPhVqZpMQqIFQhwsHLf8kyZfx1cNpw1pHo1eV6PGEW7EhIBFi3aYZraFndAXqg==", + "license": "Apache-2.0", + "dependencies": { + "google-auth-library": "^10.3.0", + "p-retry": "^4.6.2", + "protobufjs": "^7.5.4", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.25.2" + }, + "peerDependenciesMeta": { + "@modelcontextprotocol/sdk": { + "optional": true + } + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "http://localhost:4873/@humanfs/core/-/core-0.19.1.tgz", @@ -1532,6 +1699,471 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "http://localhost:4873/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "http://localhost:4873/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "http://localhost:4873/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "http://localhost:4873/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "http://localhost:4873/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "http://localhost:4873/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "http://localhost:4873/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "http://localhost:4873/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "http://localhost:4873/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "http://localhost:4873/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "http://localhost:4873/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "http://localhost:4873/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "http://localhost:4873/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "http://localhost:4873/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "http://localhost:4873/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "http://localhost:4873/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "http://localhost:4873/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "http://localhost:4873/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "http://localhost:4873/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "http://localhost:4873/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "http://localhost:4873/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "http://localhost:4873/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "http://localhost:4873/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "http://localhost:4873/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "http://localhost:4873/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@inquirer/ansi": { "version": "1.0.2", "resolved": "http://localhost:4873/@inquirer/ansi/-/ansi-1.0.2.tgz", @@ -1991,6 +2623,18 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "http://localhost:4873/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "http://localhost:4873/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -2581,6 +3225,386 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@livekit/agents": { + "version": "1.2.1", + "resolved": "http://localhost:4873/@livekit/agents/-/agents-1.2.1.tgz", + "integrity": "sha512-2IQsJ6I9FqENL9E3sE4ttSukdAyUT6LMc+BamLkXM9/3paI2VEwR0Em+4e/uYptBc9X6nk6FWed6Axa8ZKa+gg==", + "license": "Apache-2.0", + "dependencies": { + "@bufbuild/protobuf": "^1.10.0", + "@ffmpeg-installer/ffmpeg": "^1.1.0", + "@livekit/mutex": "^1.1.1", + "@livekit/protocol": "^1.45.1", + "@livekit/throws-transformer": "0.0.0-20260320165515", + "@livekit/typed-emitter": "^3.0.0", + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/api-logs": "^0.54.0", + "@opentelemetry/core": "^2.2.0", + "@opentelemetry/exporter-logs-otlp-proto": "^0.54.0", + "@opentelemetry/exporter-trace-otlp-proto": "^0.54.0", + "@opentelemetry/instrumentation-pino": "^0.43.0", + "@opentelemetry/otlp-exporter-base": "^0.208.0", + "@opentelemetry/resources": "^1.28.0", + "@opentelemetry/sdk-logs": "^0.54.0", + "@opentelemetry/sdk-trace-base": "^1.28.0", + "@opentelemetry/sdk-trace-node": "^1.28.0", + "@opentelemetry/semantic-conventions": "^1.28.0", + "@types/pidusage": "^2.0.5", + "commander": "^12.0.0", + "fluent-ffmpeg": "^2.1.3", + "form-data": "^4.0.5", + "heap-js": "^2.6.0", + "json-schema": "^0.4.0", + "livekit-server-sdk": "^2.14.1", + "ofetch": "^1.5.1", + "openai": "^6.8.1", + "pidusage": "^4.0.1", + "pino": "^8.19.0", + "pino-pretty": "^11.0.0", + "sharp": "0.34.5", + "uuid": "^11.1.0", + "ws": "^8.18.0", + "zod-to-json-schema": "^3.24.6" + }, + "peerDependencies": { + "@livekit/rtc-node": "^0.13.24", + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/@livekit/agents-plugin-google": { + "version": "1.2.1", + "resolved": "http://localhost:4873/@livekit/agents-plugin-google/-/agents-plugin-google-1.2.1.tgz", + "integrity": "sha512-JBK3qSKmwopZs7WxNZA2RXYXphPCo3oPOtjDEEN9aYCU9aeM41gYusfFMsdoOs02xJcLlqMhHUx3bGxZACtfCA==", + "license": "Apache-2.0", + "dependencies": { + "@google/genai": "^1.44.0", + "@livekit/mutex": "^1.1.1", + "@types/json-schema": "^7.0.15", + "json-schema": "^0.4.0" + }, + "peerDependencies": { + "@livekit/agents": "1.2.1", + "@livekit/rtc-node": "^0.13.24" + } + }, + "node_modules/@livekit/agents-plugin-silero": { + "version": "1.2.1", + "resolved": "http://localhost:4873/@livekit/agents-plugin-silero/-/agents-plugin-silero-1.2.1.tgz", + "integrity": "sha512-2FXT5CkVWOhBfIZUP04S2h6WBsinga6Xd6PKVJxbmaYNZmJK/8Z22qfNDaHbxZxYxDDB2E/uXhOHOlUgj+AP3A==", + "license": "Apache-2.0", + "dependencies": { + "onnxruntime-node": "1.21.0", + "ws": "^8.16.0" + }, + "peerDependencies": { + "@livekit/agents": "1.2.1", + "@livekit/rtc-node": "^0.13.24" + } + }, + "node_modules/@livekit/agents/node_modules/commander": { + "version": "12.1.0", + "resolved": "http://localhost:4873/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@livekit/mutex": { + "version": "1.1.1", + "resolved": "http://localhost:4873/@livekit/mutex/-/mutex-1.1.1.tgz", + "integrity": "sha512-EsshAucklmpuUAfkABPxJNhzj9v2sG7JuzFDL4ML1oJQSV14sqrpTYnsaOudMAw9yOaW53NU3QQTlUQoRs4czw==", + "license": "Apache-2.0" + }, + "node_modules/@livekit/protocol": { + "version": "1.45.1", + "resolved": "http://localhost:4873/@livekit/protocol/-/protocol-1.45.1.tgz", + "integrity": "sha512-sr6p0TwKofHO5KW6kUzjq4hH2de4Al5scQo824xFnyI1XYo0qQn6fTG+bdr+Uj4EedjYAOqjezwUju5OErVIRA==", + "license": "Apache-2.0", + "dependencies": { + "@bufbuild/protobuf": "^1.10.0" + } + }, + "node_modules/@livekit/rtc-node": { + "version": "0.13.24", + "resolved": "http://localhost:4873/@livekit/rtc-node/-/rtc-node-0.13.24.tgz", + "integrity": "sha512-06pF8YJlJk11R6J7kFXFpwV8etpbmCskoXFvwfwcDDixMqaP6qtS5srq3G23mDaRjx7ofz/PXg2GtiZbqNGT5A==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@bufbuild/protobuf": "^1.10.1", + "@datastructures-js/deque": "1.0.8", + "@livekit/mutex": "^1.0.0", + "@livekit/typed-emitter": "^3.0.0", + "pino": "^9.0.0", + "pino-pretty": "^13.0.0" + }, + "engines": { + "node": ">= 18" + }, + "optionalDependencies": { + "@livekit/rtc-node-darwin-arm64": "0.13.24", + "@livekit/rtc-node-darwin-x64": "0.13.24", + "@livekit/rtc-node-linux-arm64-gnu": "0.13.24", + "@livekit/rtc-node-linux-x64-gnu": "0.13.24", + "@livekit/rtc-node-win32-x64-msvc": "0.13.24" + } + }, + "node_modules/@livekit/rtc-node-darwin-arm64": { + "version": "0.13.24", + "resolved": "http://localhost:4873/@livekit/rtc-node-darwin-arm64/-/rtc-node-darwin-arm64-0.13.24.tgz", + "integrity": "sha512-gm5xOpGu6Rj/mNU2jEijcGhQGN2GdxV2dNYQm3NCKN7ow0BmMFZvXSCAWOWf+9oTutPXHnrc7EN1mt2v+lfqhA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@livekit/rtc-node-darwin-x64": { + "version": "0.13.24", + "resolved": "http://localhost:4873/@livekit/rtc-node-darwin-x64/-/rtc-node-darwin-x64-0.13.24.tgz", + "integrity": "sha512-jZSK5lHDp7+u0jby7PEWMzbxc0F0nLx6FT3FVjuMlT13ZY6QWKDUUCFbfDOtbdhiOZJYc5A4SwvubY6woEJXTg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@livekit/rtc-node-linux-arm64-gnu": { + "version": "0.13.24", + "resolved": "http://localhost:4873/@livekit/rtc-node-linux-arm64-gnu/-/rtc-node-linux-arm64-gnu-0.13.24.tgz", + "integrity": "sha512-I+IeZET2h+viZ48moEFF0EWDHa+kLii5yuEsw38ya4mHZaZtlfbzrYKGKdONqbI9M9ldvv8XXuD0wFPjuH5CZw==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@livekit/rtc-node-linux-x64-gnu": { + "version": "0.13.24", + "resolved": "http://localhost:4873/@livekit/rtc-node-linux-x64-gnu/-/rtc-node-linux-x64-gnu-0.13.24.tgz", + "integrity": "sha512-vKOxzN/SsrtV8zIVwZCi31bZUhlb6RhJZ0NnY5MwKGSRFPi7Dwt8fmr0Vh0YmsY/p+4eZjKxvFmy7L3WVE54zw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@livekit/rtc-node-win32-x64-msvc": { + "version": "0.13.24", + "resolved": "http://localhost:4873/@livekit/rtc-node-win32-x64-msvc/-/rtc-node-win32-x64-msvc-0.13.24.tgz", + "integrity": "sha512-yTzqwndq2oKLUkXW2i/BkZMJC6kZOpRO/DKvkkKQvqc3Q+JuWz1m48GmyjIwTOKF28QjqEU3+IrnD65Uu+mFOg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@livekit/rtc-node/node_modules/fast-copy": { + "version": "4.0.2", + "resolved": "http://localhost:4873/fast-copy/-/fast-copy-4.0.2.tgz", + "integrity": "sha512-ybA6PDXIXOXivLJK/z9e+Otk7ve13I4ckBvGO5I2RRmBU1gMHLVDJYEuJYhGwez7YNlYji2M2DvVU+a9mSFDlw==", + "license": "MIT", + "peer": true + }, + "node_modules/@livekit/rtc-node/node_modules/pino": { + "version": "9.14.0", + "resolved": "http://localhost:4873/pino/-/pino-9.14.0.tgz", + "integrity": "sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==", + "license": "MIT", + "peer": true, + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/@livekit/rtc-node/node_modules/pino-abstract-transport": { + "version": "2.0.0", + "resolved": "http://localhost:4873/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", + "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", + "license": "MIT", + "peer": true, + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/@livekit/rtc-node/node_modules/pino-pretty": { + "version": "13.1.3", + "resolved": "http://localhost:4873/pino-pretty/-/pino-pretty-13.1.3.tgz", + "integrity": "sha512-ttXRkkOz6WWC95KeY9+xxWL6AtImwbyMHrL1mSwqwW9u+vLp/WIElvHvCSDg0xO/Dzrggz1zv3rN5ovTRVowKg==", + "license": "MIT", + "peer": true, + "dependencies": { + "colorette": "^2.0.7", + "dateformat": "^4.6.3", + "fast-copy": "^4.0.0", + "fast-safe-stringify": "^2.1.1", + "help-me": "^5.0.0", + "joycon": "^3.1.1", + "minimist": "^1.2.6", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^3.0.0", + "pump": "^3.0.0", + "secure-json-parse": "^4.0.0", + "sonic-boom": "^4.0.1", + "strip-json-comments": "^5.0.2" + }, + "bin": { + "pino-pretty": "bin.js" + } + }, + "node_modules/@livekit/rtc-node/node_modules/pino-pretty/node_modules/pino-abstract-transport": { + "version": "3.0.0", + "resolved": "http://localhost:4873/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz", + "integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==", + "license": "MIT", + "peer": true, + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/@livekit/rtc-node/node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "http://localhost:4873/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "license": "MIT", + "peer": true + }, + "node_modules/@livekit/rtc-node/node_modules/process-warning": { + "version": "5.0.0", + "resolved": "http://localhost:4873/process-warning/-/process-warning-5.0.0.tgz", + "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "peer": true + }, + "node_modules/@livekit/rtc-node/node_modules/secure-json-parse": { + "version": "4.1.0", + "resolved": "http://localhost:4873/secure-json-parse/-/secure-json-parse-4.1.0.tgz", + "integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/@livekit/rtc-node/node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "http://localhost:4873/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/@livekit/rtc-node/node_modules/strip-json-comments": { + "version": "5.0.3", + "resolved": "http://localhost:4873/strip-json-comments/-/strip-json-comments-5.0.3.tgz", + "integrity": "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@livekit/rtc-node/node_modules/thread-stream": { + "version": "3.1.0", + "resolved": "http://localhost:4873/thread-stream/-/thread-stream-3.1.0.tgz", + "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", + "license": "MIT", + "peer": true, + "dependencies": { + "real-require": "^0.2.0" + } + }, + "node_modules/@livekit/throws-transformer": { + "version": "0.0.0-20260320165515", + "resolved": "http://localhost:4873/@livekit/throws-transformer/-/throws-transformer-0.0.0-20260320165515.tgz", + "integrity": "sha512-3L4UKOov1VXuX6sHIBuonJTaPzsSkpqZT3htvamgUYR0pL/aJ+0piiWzTPoCx9WSfmmUUAQqjd42IPgHXQVdvQ==", + "license": "Apache-2.0", + "dependencies": { + "glob": "^13.0.0" + }, + "bin": { + "throws-check": "dist/cli.js" + }, + "peerDependencies": { + "typescript": ">=4.7.0" + } + }, + "node_modules/@livekit/typed-emitter": { + "version": "3.0.0", + "resolved": "http://localhost:4873/@livekit/typed-emitter/-/typed-emitter-3.0.0.tgz", + "integrity": "sha512-9bl0k4MgBPZu3Qu3R3xy12rmbW17e3bE9yf4YY85gJIQ3ezLEj/uzpKHWBsLaDoL5Mozz8QCgggwIBudYQWeQg==", + "license": "MIT" + }, "node_modules/@lukeed/csprng": { "version": "1.1.0", "resolved": "http://localhost:4873/@lukeed/csprng/-/csprng-1.1.0.tgz", @@ -3133,6 +4157,825 @@ "node": ">=8.0.0" } }, + "node_modules/@opentelemetry/api-logs": { + "version": "0.54.2", + "resolved": "http://localhost:4873/@opentelemetry/api-logs/-/api-logs-0.54.2.tgz", + "integrity": "sha512-4MTVwwmLgUh5QrJnZpYo6YRO5IBLAggf2h8gWDblwRagDStY13aEvt7gGk3jewrMaPlHiF83fENhIx0HO97/cQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@opentelemetry/context-async-hooks": { + "version": "1.30.1", + "resolved": "http://localhost:4873/@opentelemetry/context-async-hooks/-/context-async-hooks-1.30.1.tgz", + "integrity": "sha512-s5vvxXPVdjqS3kTLKMeBMvop9hbWkwzBpu+mUO2M7sZtlkyDJGwFe33wRKnbaYDo8ExRVBIIdwIGrqpxHuKttA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/core": { + "version": "2.6.0", + "resolved": "http://localhost:4873/@opentelemetry/core/-/core-2.6.0.tgz", + "integrity": "sha512-HLM1v2cbZ4TgYN6KEOj+Bbj8rAKriOdkF9Ed3tG25FoprSiQl7kYc+RRT6fUZGOvx0oMi5U67GoFdT+XUn8zEg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-proto": { + "version": "0.54.2", + "resolved": "http://localhost:4873/@opentelemetry/exporter-logs-otlp-proto/-/exporter-logs-otlp-proto-0.54.2.tgz", + "integrity": "sha512-agrzFbSNmIy6dhkyg41ERlEDUDqkaUJj2n/tVRFp9Tl+6wyNVPsqmwU5RWJOXpyK+lYH/znv6A47VpTeJF0lrw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.54.2", + "@opentelemetry/core": "1.27.0", + "@opentelemetry/otlp-exporter-base": "0.54.2", + "@opentelemetry/otlp-transformer": "0.54.2", + "@opentelemetry/resources": "1.27.0", + "@opentelemetry/sdk-logs": "0.54.2", + "@opentelemetry/sdk-trace-base": "1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/core": { + "version": "1.27.0", + "resolved": "http://localhost:4873/@opentelemetry/core/-/core-1.27.0.tgz", + "integrity": "sha512-yQPKnK5e+76XuiqUH/gKyS8wv/7qITd5ln56QkBTf3uggr0VkXOXfcaAuG330UfdYu83wsyoBwqwxigpIG+Jkg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/otlp-exporter-base": { + "version": "0.54.2", + "resolved": "http://localhost:4873/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.54.2.tgz", + "integrity": "sha512-NrNyxu6R/bGAwanhz1HI0aJWKR6xUED4TjCH4iWMlAfyRukGbI9Kt/Akd2sYLwRKNhfS+sKetKGCUQPMDyYYMA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.27.0", + "@opentelemetry/otlp-transformer": "0.54.2" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/resources": { + "version": "1.27.0", + "resolved": "http://localhost:4873/@opentelemetry/resources/-/resources-1.27.0.tgz", + "integrity": "sha512-jOwt2VJ/lUD5BLc+PMNymDrUCpm5PKi1E9oSVYAvz01U/VdndGmrtV3DU1pG4AwlYhJRHbHfOUIlpBeXCPw6QQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.27.0", + "@opentelemetry/semantic-conventions": "1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/sdk-trace-base": { + "version": "1.27.0", + "resolved": "http://localhost:4873/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.27.0.tgz", + "integrity": "sha512-btz6XTQzwsyJjombpeqCX6LhiMQYpzt2pIYNPnw0IPO/3AhT6yjnf8Mnv3ZC2A4eRYOjqrg+bfaXg9XHDRJDWQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.27.0", + "@opentelemetry/resources": "1.27.0", + "@opentelemetry/semantic-conventions": "1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.27.0", + "resolved": "http://localhost:4873/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz", + "integrity": "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-proto": { + "version": "0.54.2", + "resolved": "http://localhost:4873/@opentelemetry/exporter-trace-otlp-proto/-/exporter-trace-otlp-proto-0.54.2.tgz", + "integrity": "sha512-XSmm1N2wAhoWDXP1q/N6kpLebWaxl6VIADv4WA5QWKHLRpF3gLz5NAWNJBR8ygsvv8jQcrwnXgwfnJ18H3v1fg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.27.0", + "@opentelemetry/otlp-exporter-base": "0.54.2", + "@opentelemetry/otlp-transformer": "0.54.2", + "@opentelemetry/resources": "1.27.0", + "@opentelemetry/sdk-trace-base": "1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-proto/node_modules/@opentelemetry/core": { + "version": "1.27.0", + "resolved": "http://localhost:4873/@opentelemetry/core/-/core-1.27.0.tgz", + "integrity": "sha512-yQPKnK5e+76XuiqUH/gKyS8wv/7qITd5ln56QkBTf3uggr0VkXOXfcaAuG330UfdYu83wsyoBwqwxigpIG+Jkg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-proto/node_modules/@opentelemetry/otlp-exporter-base": { + "version": "0.54.2", + "resolved": "http://localhost:4873/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.54.2.tgz", + "integrity": "sha512-NrNyxu6R/bGAwanhz1HI0aJWKR6xUED4TjCH4iWMlAfyRukGbI9Kt/Akd2sYLwRKNhfS+sKetKGCUQPMDyYYMA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.27.0", + "@opentelemetry/otlp-transformer": "0.54.2" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-proto/node_modules/@opentelemetry/resources": { + "version": "1.27.0", + "resolved": "http://localhost:4873/@opentelemetry/resources/-/resources-1.27.0.tgz", + "integrity": "sha512-jOwt2VJ/lUD5BLc+PMNymDrUCpm5PKi1E9oSVYAvz01U/VdndGmrtV3DU1pG4AwlYhJRHbHfOUIlpBeXCPw6QQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.27.0", + "@opentelemetry/semantic-conventions": "1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-proto/node_modules/@opentelemetry/sdk-trace-base": { + "version": "1.27.0", + "resolved": "http://localhost:4873/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.27.0.tgz", + "integrity": "sha512-btz6XTQzwsyJjombpeqCX6LhiMQYpzt2pIYNPnw0IPO/3AhT6yjnf8Mnv3ZC2A4eRYOjqrg+bfaXg9XHDRJDWQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.27.0", + "@opentelemetry/resources": "1.27.0", + "@opentelemetry/semantic-conventions": "1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-proto/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.27.0", + "resolved": "http://localhost:4873/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz", + "integrity": "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@opentelemetry/instrumentation": { + "version": "0.54.2", + "resolved": "http://localhost:4873/@opentelemetry/instrumentation/-/instrumentation-0.54.2.tgz", + "integrity": "sha512-go6zpOVoZVztT9r1aPd79Fr3OWiD4N24bCPJsIKkBses8oyFo12F/Ew3UBTdIu6hsW4HC4MVEJygG6TEyJI/lg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.54.2", + "@types/shimmer": "^1.2.0", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1", + "semver": "^7.5.2", + "shimmer": "^1.2.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-pino": { + "version": "0.43.0", + "resolved": "http://localhost:4873/@opentelemetry/instrumentation-pino/-/instrumentation-pino-0.43.0.tgz", + "integrity": "sha512-jlOOgbODWRRNknWXY1VLgmqgG0SO4kLgU3XnejjO/3De4OisroAsMGk+1cRB5AQ6WZ8WLAMkMyTShaOe6j2Asw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "^0.54.0", + "@opentelemetry/core": "^1.25.0", + "@opentelemetry/instrumentation": "^0.54.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-pino/node_modules/@opentelemetry/core": { + "version": "1.30.1", + "resolved": "http://localhost:4873/@opentelemetry/core/-/core-1.30.1.tgz", + "integrity": "sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "1.28.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/instrumentation-pino/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.28.0", + "resolved": "http://localhost:4873/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz", + "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@opentelemetry/otlp-exporter-base": { + "version": "0.208.0", + "resolved": "http://localhost:4873/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.208.0.tgz", + "integrity": "sha512-gMd39gIfVb2OgxldxUtOwGJYSH8P1kVFFlJLuut32L6KgUC4gl1dMhn+YC2mGn0bDOiQYSk/uHOdSjuKp58vvA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/otlp-transformer": "0.208.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-exporter-base/node_modules/@opentelemetry/api-logs": { + "version": "0.208.0", + "resolved": "http://localhost:4873/@opentelemetry/api-logs/-/api-logs-0.208.0.tgz", + "integrity": "sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/otlp-exporter-base/node_modules/@opentelemetry/core": { + "version": "2.2.0", + "resolved": "http://localhost:4873/@opentelemetry/core/-/core-2.2.0.tgz", + "integrity": "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/otlp-exporter-base/node_modules/@opentelemetry/otlp-transformer": { + "version": "0.208.0", + "resolved": "http://localhost:4873/@opentelemetry/otlp-transformer/-/otlp-transformer-0.208.0.tgz", + "integrity": "sha512-DCFPY8C6lAQHUNkzcNT9R+qYExvsk6C5Bto2pbNxgicpcSWbe2WHShLxkOxIdNcBiYPdVHv/e7vH7K6TI+C+fQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.208.0", + "@opentelemetry/core": "2.2.0", + "@opentelemetry/resources": "2.2.0", + "@opentelemetry/sdk-logs": "0.208.0", + "@opentelemetry/sdk-metrics": "2.2.0", + "@opentelemetry/sdk-trace-base": "2.2.0", + "protobufjs": "^7.3.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-exporter-base/node_modules/@opentelemetry/resources": { + "version": "2.2.0", + "resolved": "http://localhost:4873/@opentelemetry/resources/-/resources-2.2.0.tgz", + "integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/otlp-exporter-base/node_modules/@opentelemetry/sdk-logs": { + "version": "0.208.0", + "resolved": "http://localhost:4873/@opentelemetry/sdk-logs/-/sdk-logs-0.208.0.tgz", + "integrity": "sha512-QlAyL1jRpOeaqx7/leG1vJMp84g0xKP6gJmfELBpnI4O/9xPX+Hu5m1POk9Kl+veNkyth5t19hRlN6tNY1sjbA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.208.0", + "@opentelemetry/core": "2.2.0", + "@opentelemetry/resources": "2.2.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.4.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/otlp-exporter-base/node_modules/@opentelemetry/sdk-metrics": { + "version": "2.2.0", + "resolved": "http://localhost:4873/@opentelemetry/sdk-metrics/-/sdk-metrics-2.2.0.tgz", + "integrity": "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/resources": "2.2.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/otlp-exporter-base/node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.2.0", + "resolved": "http://localhost:4873/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.2.0.tgz", + "integrity": "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.2.0", + "@opentelemetry/resources": "2.2.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer": { + "version": "0.54.2", + "resolved": "http://localhost:4873/@opentelemetry/otlp-transformer/-/otlp-transformer-0.54.2.tgz", + "integrity": "sha512-2tIjahJlMRRUz0A2SeE+qBkeBXBFkSjR0wqJ08kuOqaL8HNGan5iZf+A8cfrfmZzPUuMKCyY9I+okzFuFs6gKQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.54.2", + "@opentelemetry/core": "1.27.0", + "@opentelemetry/resources": "1.27.0", + "@opentelemetry/sdk-logs": "0.54.2", + "@opentelemetry/sdk-metrics": "1.27.0", + "@opentelemetry/sdk-trace-base": "1.27.0", + "protobufjs": "^7.3.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/core": { + "version": "1.27.0", + "resolved": "http://localhost:4873/@opentelemetry/core/-/core-1.27.0.tgz", + "integrity": "sha512-yQPKnK5e+76XuiqUH/gKyS8wv/7qITd5ln56QkBTf3uggr0VkXOXfcaAuG330UfdYu83wsyoBwqwxigpIG+Jkg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/resources": { + "version": "1.27.0", + "resolved": "http://localhost:4873/@opentelemetry/resources/-/resources-1.27.0.tgz", + "integrity": "sha512-jOwt2VJ/lUD5BLc+PMNymDrUCpm5PKi1E9oSVYAvz01U/VdndGmrtV3DU1pG4AwlYhJRHbHfOUIlpBeXCPw6QQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.27.0", + "@opentelemetry/semantic-conventions": "1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/sdk-trace-base": { + "version": "1.27.0", + "resolved": "http://localhost:4873/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.27.0.tgz", + "integrity": "sha512-btz6XTQzwsyJjombpeqCX6LhiMQYpzt2pIYNPnw0IPO/3AhT6yjnf8Mnv3ZC2A4eRYOjqrg+bfaXg9XHDRJDWQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.27.0", + "@opentelemetry/resources": "1.27.0", + "@opentelemetry/semantic-conventions": "1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.27.0", + "resolved": "http://localhost:4873/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz", + "integrity": "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@opentelemetry/propagator-b3": { + "version": "1.30.1", + "resolved": "http://localhost:4873/@opentelemetry/propagator-b3/-/propagator-b3-1.30.1.tgz", + "integrity": "sha512-oATwWWDIJzybAZ4pO76ATN5N6FFbOA1otibAVlS8v90B4S1wClnhRUk7K+2CHAwN1JKYuj4jh/lpCEG5BAqFuQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.30.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/propagator-b3/node_modules/@opentelemetry/core": { + "version": "1.30.1", + "resolved": "http://localhost:4873/@opentelemetry/core/-/core-1.30.1.tgz", + "integrity": "sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "1.28.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/propagator-b3/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.28.0", + "resolved": "http://localhost:4873/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz", + "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@opentelemetry/propagator-jaeger": { + "version": "1.30.1", + "resolved": "http://localhost:4873/@opentelemetry/propagator-jaeger/-/propagator-jaeger-1.30.1.tgz", + "integrity": "sha512-Pj/BfnYEKIOImirH76M4hDaBSx6HyZ2CXUqk+Kj02m6BB80c/yo4BdWkn/1gDFfU+YPY+bPR2U0DKBfdxCKwmg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.30.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/propagator-jaeger/node_modules/@opentelemetry/core": { + "version": "1.30.1", + "resolved": "http://localhost:4873/@opentelemetry/core/-/core-1.30.1.tgz", + "integrity": "sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "1.28.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/propagator-jaeger/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.28.0", + "resolved": "http://localhost:4873/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz", + "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@opentelemetry/resources": { + "version": "1.30.1", + "resolved": "http://localhost:4873/@opentelemetry/resources/-/resources-1.30.1.tgz", + "integrity": "sha512-5UxZqiAgLYGFjS4s9qm5mBVo433u+dSPUFWVWXmLAD4wB65oMCoXaJP1KJa9DIYYMeHu3z4BZcStG3LC593cWA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.30.1", + "@opentelemetry/semantic-conventions": "1.28.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/resources/node_modules/@opentelemetry/core": { + "version": "1.30.1", + "resolved": "http://localhost:4873/@opentelemetry/core/-/core-1.30.1.tgz", + "integrity": "sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "1.28.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/resources/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.28.0", + "resolved": "http://localhost:4873/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz", + "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@opentelemetry/sdk-logs": { + "version": "0.54.2", + "resolved": "http://localhost:4873/@opentelemetry/sdk-logs/-/sdk-logs-0.54.2.tgz", + "integrity": "sha512-yIbYqDLS/AtBbPjCjh6eSToGNRMqW2VR8RrKEy+G+J7dFG7pKoptTH5T+XlKPleP9NY8JZYIpgJBlI+Osi0rFw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.54.2", + "@opentelemetry/core": "1.27.0", + "@opentelemetry/resources": "1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.4.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/core": { + "version": "1.27.0", + "resolved": "http://localhost:4873/@opentelemetry/core/-/core-1.27.0.tgz", + "integrity": "sha512-yQPKnK5e+76XuiqUH/gKyS8wv/7qITd5ln56QkBTf3uggr0VkXOXfcaAuG330UfdYu83wsyoBwqwxigpIG+Jkg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/resources": { + "version": "1.27.0", + "resolved": "http://localhost:4873/@opentelemetry/resources/-/resources-1.27.0.tgz", + "integrity": "sha512-jOwt2VJ/lUD5BLc+PMNymDrUCpm5PKi1E9oSVYAvz01U/VdndGmrtV3DU1pG4AwlYhJRHbHfOUIlpBeXCPw6QQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.27.0", + "@opentelemetry/semantic-conventions": "1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.27.0", + "resolved": "http://localhost:4873/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz", + "integrity": "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@opentelemetry/sdk-metrics": { + "version": "1.27.0", + "resolved": "http://localhost:4873/@opentelemetry/sdk-metrics/-/sdk-metrics-1.27.0.tgz", + "integrity": "sha512-JzWgzlutoXCydhHWIbLg+r76m+m3ncqvkCcsswXAQ4gqKS+LOHKhq+t6fx1zNytvLuaOUBur7EvWxECc4jPQKg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.27.0", + "@opentelemetry/resources": "1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-metrics/node_modules/@opentelemetry/core": { + "version": "1.27.0", + "resolved": "http://localhost:4873/@opentelemetry/core/-/core-1.27.0.tgz", + "integrity": "sha512-yQPKnK5e+76XuiqUH/gKyS8wv/7qITd5ln56QkBTf3uggr0VkXOXfcaAuG330UfdYu83wsyoBwqwxigpIG+Jkg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-metrics/node_modules/@opentelemetry/resources": { + "version": "1.27.0", + "resolved": "http://localhost:4873/@opentelemetry/resources/-/resources-1.27.0.tgz", + "integrity": "sha512-jOwt2VJ/lUD5BLc+PMNymDrUCpm5PKi1E9oSVYAvz01U/VdndGmrtV3DU1pG4AwlYhJRHbHfOUIlpBeXCPw6QQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.27.0", + "@opentelemetry/semantic-conventions": "1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-metrics/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.27.0", + "resolved": "http://localhost:4873/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz", + "integrity": "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@opentelemetry/sdk-trace-base": { + "version": "1.30.1", + "resolved": "http://localhost:4873/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.30.1.tgz", + "integrity": "sha512-jVPgBbH1gCy2Lb7X0AVQ8XAfgg0pJ4nvl8/IiQA6nxOsPvS+0zMJaFSs2ltXe0J6C8dqjcnpyqINDJmU30+uOg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.30.1", + "@opentelemetry/resources": "1.30.1", + "@opentelemetry/semantic-conventions": "1.28.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-base/node_modules/@opentelemetry/core": { + "version": "1.30.1", + "resolved": "http://localhost:4873/@opentelemetry/core/-/core-1.30.1.tgz", + "integrity": "sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "1.28.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-base/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.28.0", + "resolved": "http://localhost:4873/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz", + "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@opentelemetry/sdk-trace-node": { + "version": "1.30.1", + "resolved": "http://localhost:4873/@opentelemetry/sdk-trace-node/-/sdk-trace-node-1.30.1.tgz", + "integrity": "sha512-cBjYOINt1JxXdpw1e5MlHmFRc5fgj4GW/86vsKFxJCJ8AL4PdVtYH41gWwl4qd4uQjqEL1oJVrXkSy5cnduAnQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/context-async-hooks": "1.30.1", + "@opentelemetry/core": "1.30.1", + "@opentelemetry/propagator-b3": "1.30.1", + "@opentelemetry/propagator-jaeger": "1.30.1", + "@opentelemetry/sdk-trace-base": "1.30.1", + "semver": "^7.5.2" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-node/node_modules/@opentelemetry/core": { + "version": "1.30.1", + "resolved": "http://localhost:4873/@opentelemetry/core/-/core-1.30.1.tgz", + "integrity": "sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "1.28.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-node/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.28.0", + "resolved": "http://localhost:4873/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz", + "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.40.0", + "resolved": "http://localhost:4873/@opentelemetry/semantic-conventions/-/semantic-conventions-1.40.0.tgz", + "integrity": "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, "node_modules/@paralleldrive/cuid2": { "version": "2.3.1", "resolved": "http://localhost:4873/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", @@ -3143,6 +4986,13 @@ "@noble/hashes": "^1.1.5" } }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "http://localhost:4873/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "license": "MIT", + "peer": true + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "http://localhost:4873/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -3167,6 +5017,70 @@ "url": "https://opencollective.com/pkgr" } }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "http://localhost:4873/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "http://localhost:4873/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "http://localhost:4873/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "http://localhost:4873/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "http://localhost:4873/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "http://localhost:4873/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "http://localhost:4873/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "http://localhost:4873/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "http://localhost:4873/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "http://localhost:4873/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" + }, "node_modules/@sinclair/typebox": { "version": "0.34.48", "resolved": "http://localhost:4873/@sinclair/typebox/-/typebox-0.34.48.tgz", @@ -3453,7 +5367,6 @@ "version": "7.0.15", "resolved": "http://localhost:4873/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, "license": "MIT" }, "node_modules/@types/methods": { @@ -3472,6 +5385,12 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/pidusage": { + "version": "2.0.5", + "resolved": "http://localhost:4873/@types/pidusage/-/pidusage-2.0.5.tgz", + "integrity": "sha512-MIiyZI4/MK9UGUXWt0jJcCZhVw7YdhBuTOuqP/BjuLDLZ2PmmViMIQgZiWxtaMicQfAz/kMrZ5T7PKxFSkTeUA==", + "license": "MIT" + }, "node_modules/@types/qs": { "version": "6.15.0", "resolved": "http://localhost:4873/@types/qs/-/qs-6.15.0.tgz", @@ -3486,6 +5405,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "http://localhost:4873/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "license": "MIT" + }, "node_modules/@types/send": { "version": "1.2.1", "resolved": "http://localhost:4873/@types/send/-/send-1.2.1.tgz", @@ -3507,6 +5432,12 @@ "@types/node": "*" } }, + "node_modules/@types/shimmer": { + "version": "1.2.0", + "resolved": "http://localhost:4873/@types/shimmer/-/shimmer-1.2.0.tgz", + "integrity": "sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==", + "license": "MIT" + }, "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "http://localhost:4873/@types/stack-utils/-/stack-utils-2.0.3.tgz", @@ -4306,6 +6237,18 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "http://localhost:4873/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/accepts": { "version": "2.0.0", "resolved": "http://localhost:4873/accepts/-/accepts-2.0.0.tgz", @@ -4323,7 +6266,6 @@ "version": "8.16.0", "resolved": "http://localhost:4873/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", - "dev": true, "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -4332,6 +6274,15 @@ "node": ">=0.4.0" } }, + "node_modules/acorn-import-attributes": { + "version": "1.9.5", + "resolved": "http://localhost:4873/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", + "license": "MIT", + "peerDependencies": { + "acorn": "^8" + } + }, "node_modules/acorn-import-phases": { "version": "1.0.4", "resolved": "http://localhost:4873/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", @@ -4368,6 +6319,15 @@ "node": ">=0.4.0" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "http://localhost:4873/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/ai": { "version": "6.0.116", "resolved": "http://localhost:4873/ai/-/ai-6.0.116.tgz", @@ -4578,12 +6538,26 @@ "dev": true, "license": "MIT" }, + "node_modules/async": { + "version": "0.2.10", + "resolved": "http://localhost:4873/async/-/async-0.2.10.tgz", + "integrity": "sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ==" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "http://localhost:4873/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "license": "MIT" }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "http://localhost:4873/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/axios": { "version": "1.13.6", "resolved": "http://localhost:4873/axios/-/axios-1.13.6.tgz", @@ -4705,7 +6679,6 @@ "version": "1.5.1", "resolved": "http://localhost:4873/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, "funding": [ { "type": "github", @@ -4744,6 +6717,15 @@ "node": ">=6.0.0" } }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "http://localhost:4873/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/bl": { "version": "4.1.0", "resolved": "http://localhost:4873/bl/-/bl-4.1.0.tgz", @@ -4780,6 +6762,13 @@ "url": "https://opencollective.com/express" } }, + "node_modules/boolean": { + "version": "3.2.0", + "resolved": "http://localhost:4873/boolean/-/boolean-3.2.0.tgz", + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT" + }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "http://localhost:4873/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -4886,6 +6875,12 @@ "ieee754": "^1.1.13" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "http://localhost:4873/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "http://localhost:4873/buffer-from/-/buffer-from-1.1.2.tgz", @@ -4961,6 +6956,48 @@ "node": ">=6" } }, + "node_modules/camelcase-keys": { + "version": "9.1.3", + "resolved": "http://localhost:4873/camelcase-keys/-/camelcase-keys-9.1.3.tgz", + "integrity": "sha512-Rircqi9ch8AnZscQcsA1C47NFdaO3wukpmIRzYcDOrmvgt78hM/sj5pZhZNec2NM12uk5vTwRHZ4anGcrC4ZTg==", + "license": "MIT", + "dependencies": { + "camelcase": "^8.0.0", + "map-obj": "5.0.0", + "quick-lru": "^6.1.1", + "type-fest": "^4.3.2" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/camelcase-keys/node_modules/camelcase": { + "version": "8.0.0", + "resolved": "http://localhost:4873/camelcase/-/camelcase-8.0.0.tgz", + "integrity": "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/camelcase-keys/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "http://localhost:4873/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001779", "resolved": "http://localhost:4873/caniuse-lite/-/caniuse-lite-1.0.30001779.tgz", @@ -5032,6 +7069,15 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "http://localhost:4873/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/chrome-trace-event": { "version": "1.0.4", "resolved": "http://localhost:4873/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", @@ -5207,6 +7253,12 @@ "dev": true, "license": "MIT" }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "http://localhost:4873/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "license": "MIT" + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "http://localhost:4873/combined-stream/-/combined-stream-1.0.8.tgz", @@ -5412,6 +7464,24 @@ "node": ">= 8" } }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "http://localhost:4873/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/dateformat": { + "version": "4.6.3", + "resolved": "http://localhost:4873/dateformat/-/dateformat-4.6.3.tgz", + "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "http://localhost:4873/debug/-/debug-4.4.3.tgz", @@ -5474,6 +7544,40 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "http://localhost:4873/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "http://localhost:4873/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "http://localhost:4873/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -5501,6 +7605,21 @@ "node": ">= 0.8" } }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "http://localhost:4873/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "http://localhost:4873/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "http://localhost:4873/detect-newline/-/detect-newline-3.1.0.tgz", @@ -5511,6 +7630,12 @@ "node": ">=8" } }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "http://localhost:4873/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "license": "MIT" + }, "node_modules/dezalgo": { "version": "1.0.4", "resolved": "http://localhost:4873/dezalgo/-/dezalgo-1.0.4.tgz", @@ -5592,6 +7717,15 @@ "dev": true, "license": "MIT" }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "http://localhost:4873/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "http://localhost:4873/ee-first/-/ee-first-1.1.1.tgz", @@ -5634,6 +7768,15 @@ "node": ">= 0.8" } }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "http://localhost:4873/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/engine.io": { "version": "6.6.6", "resolved": "http://localhost:4873/engine.io/-/engine.io-6.6.6.tgz", @@ -5783,6 +7926,12 @@ "node": ">= 0.4" } }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "http://localhost:4873/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "license": "MIT" + }, "node_modules/esbuild": { "version": "0.27.4", "resolved": "http://localhost:4873/esbuild/-/esbuild-0.27.4.tgz", @@ -5845,7 +7994,6 @@ "version": "4.0.0", "resolved": "http://localhost:4873/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -6078,11 +8226,25 @@ "node": ">= 0.6" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "http://localhost:4873/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/eventemitter2": { + "version": "6.4.9", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.9.tgz", + "integrity": "sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==", + "license": "MIT" + }, "node_modules/events": { "version": "3.3.0", "resolved": "http://localhost:4873/events/-/events-3.3.0.tgz", "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.8.x" @@ -6199,6 +8361,18 @@ "url": "https://opencollective.com/express" } }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "http://localhost:4873/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fast-copy": { + "version": "3.0.2", + "resolved": "http://localhost:4873/fast-copy/-/fast-copy-3.0.2.tgz", + "integrity": "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==", + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "http://localhost:4873/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -6227,6 +8401,15 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-redact": { + "version": "3.5.0", + "resolved": "http://localhost:4873/fast-redact/-/fast-redact-3.5.0.tgz", + "integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/fast-safe-stringify": { "version": "2.1.1", "resolved": "http://localhost:4873/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", @@ -6278,6 +8461,29 @@ } } }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "http://localhost:4873/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "http://localhost:4873/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -6381,6 +8587,32 @@ "dev": true, "license": "ISC" }, + "node_modules/fluent-ffmpeg": { + "version": "2.1.3", + "resolved": "http://localhost:4873/fluent-ffmpeg/-/fluent-ffmpeg-2.1.3.tgz", + "integrity": "sha512-Be3narBNt2s6bsaqP6Jzq91heDgOEaDCJAXcE3qcma/EJBSy5FB4cvO31XBInuAuKBx8Kptf8dkhjK0IOru39Q==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT", + "dependencies": { + "async": "^0.2.9", + "which": "^1.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/fluent-ffmpeg/node_modules/which": { + "version": "1.3.1", + "resolved": "http://localhost:4873/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, "node_modules/follow-redirects": { "version": "1.15.11", "resolved": "http://localhost:4873/follow-redirects/-/follow-redirects-1.15.11.tgz", @@ -6483,6 +8715,18 @@ "node": ">= 0.6" } }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "http://localhost:4873/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/formidable": { "version": "3.5.4", "resolved": "http://localhost:4873/formidable/-/formidable-3.5.4.tgz", @@ -6572,6 +8816,34 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gaxios": { + "version": "7.1.4", + "resolved": "http://localhost:4873/gaxios/-/gaxios-7.1.4.tgz", + "integrity": "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gcp-metadata": { + "version": "8.1.2", + "resolved": "http://localhost:4873/gcp-metadata/-/gcp-metadata-8.1.2.tgz", + "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "http://localhost:4873/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -6669,7 +8941,6 @@ "version": "13.0.0", "resolved": "http://localhost:4873/glob/-/glob-13.0.0.tgz", "integrity": "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==", - "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "minimatch": "^10.1.1", @@ -6707,7 +8978,6 @@ "version": "4.0.4", "resolved": "http://localhost:4873/balanced-match/-/balanced-match-4.0.4.tgz", "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, "license": "MIT", "engines": { "node": "18 || 20 || >=22" @@ -6717,7 +8987,6 @@ "version": "5.0.4", "resolved": "http://localhost:4873/brace-expansion/-/brace-expansion-5.0.4.tgz", "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" @@ -6730,7 +8999,6 @@ "version": "10.2.4", "resolved": "http://localhost:4873/minimatch/-/minimatch-10.2.4.tgz", "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", - "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "brace-expansion": "^5.0.2" @@ -6742,6 +9010,23 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/global-agent": { + "version": "3.0.0", + "resolved": "http://localhost:4873/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", + "license": "BSD-3-Clause", + "dependencies": { + "boolean": "^3.0.1", + "es6-error": "^4.1.1", + "matcher": "^3.0.0", + "roarr": "^2.15.3", + "semver": "^7.3.2", + "serialize-error": "^7.0.1" + }, + "engines": { + "node": ">=10.0" + } + }, "node_modules/globals": { "version": "16.5.0", "resolved": "http://localhost:4873/globals/-/globals-16.5.0.tgz", @@ -6755,6 +9040,48 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "http://localhost:4873/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/google-auth-library": { + "version": "10.6.2", + "resolved": "http://localhost:4873/google-auth-library/-/google-auth-library-10.6.2.tgz", + "integrity": "sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.1.4", + "gcp-metadata": "8.1.2", + "google-logging-utils": "1.1.3", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-logging-utils": { + "version": "1.1.3", + "resolved": "http://localhost:4873/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "http://localhost:4873/gopd/-/gopd-1.2.0.tgz", @@ -6816,6 +9143,18 @@ "node": ">=8" } }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "http://localhost:4873/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "http://localhost:4873/has-symbols/-/has-symbols-1.1.0.tgz", @@ -6843,6 +9182,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hash-it": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/hash-it/-/hash-it-6.0.1.tgz", + "integrity": "sha512-qhl8+l4Zwi1eLlL3lja5ywmDQnBzLEJxd0QJoAVIgZpgQbdtVZrN5ypB0y3VHwBlvAalpcbM2/A6x7oUks5zNg==", + "license": "MIT" + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "http://localhost:4873/hasown/-/hasown-2.0.2.tgz", @@ -6855,6 +9200,21 @@ "node": ">= 0.4" } }, + "node_modules/heap-js": { + "version": "2.7.1", + "resolved": "http://localhost:4873/heap-js/-/heap-js-2.7.1.tgz", + "integrity": "sha512-EQfezRg0NCZGNlhlDR3Evrw1FVL2G3LhU7EgPoxufQKruNBSYA8MiRPHeWbU+36o+Fhel0wMwM+sLEiBAlNLJA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/help-me": { + "version": "5.0.0", + "resolved": "http://localhost:4873/help-me/-/help-me-5.0.0.tgz", + "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==", + "license": "MIT" + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "http://localhost:4873/html-escaper/-/html-escaper-2.0.2.tgz", @@ -6882,6 +9242,19 @@ "url": "https://opencollective.com/express" } }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "http://localhost:4873/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "http://localhost:4873/human-signals/-/human-signals-2.1.0.tgz", @@ -6955,6 +9328,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/import-in-the-middle": { + "version": "1.15.0", + "resolved": "http://localhost:4873/import-in-the-middle/-/import-in-the-middle-1.15.0.tgz", + "integrity": "sha512-bpQy+CrsRmYmoPMAE/0G33iwRqwW4ouqdRg8jgbH3aKuCtOc8lxgmYXg2dMM92CRiGP660EtBcymH/eVUpCSaA==", + "license": "Apache-2.0", + "dependencies": { + "acorn": "^8.14.0", + "acorn-import-attributes": "^1.9.5", + "cjs-module-lexer": "^1.2.2", + "module-details-from-path": "^1.0.3" + } + }, + "node_modules/import-in-the-middle/node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "http://localhost:4873/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "license": "MIT" + }, "node_modules/import-local": { "version": "3.2.0", "resolved": "http://localhost:4873/import-local/-/import-local-3.2.0.tgz", @@ -7043,6 +9434,21 @@ "dev": true, "license": "MIT" }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "http://localhost:4873/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "http://localhost:4873/is-extglob/-/is-extglob-2.1.1.tgz", @@ -7142,7 +9548,6 @@ "version": "2.0.0", "resolved": "http://localhost:4873/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, "license": "ISC" }, "node_modules/istanbul-lib-coverage": { @@ -8024,6 +10429,24 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/jose": { + "version": "5.10.0", + "resolved": "http://localhost:4873/jose/-/jose-5.10.0.tgz", + "integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "http://localhost:4873/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "http://localhost:4873/js-tokens/-/js-tokens-4.0.0.tgz", @@ -8057,6 +10480,15 @@ "node": ">=6" } }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "http://localhost:4873/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "http://localhost:4873/json-buffer/-/json-buffer-3.0.1.tgz", @@ -8071,6 +10503,27 @@ "dev": true, "license": "MIT" }, + "node_modules/json-rules-engine": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/json-rules-engine/-/json-rules-engine-6.6.0.tgz", + "integrity": "sha512-jJ4eVCPnItetPiU3fTIzrrl3d2zeIXCcCy11dwWhN72YXBR2mByV1Vfbrvt6y2n+VFmxc6rtL/XhDqLKIwBx6g==", + "license": "ISC", + "dependencies": { + "clone": "^2.1.2", + "eventemitter2": "^6.4.4", + "hash-it": "^6.0.0", + "jsonpath-plus": "^7.2.0" + } + }, + "node_modules/json-rules-engine/node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, "node_modules/json-schema": { "version": "0.4.0", "resolved": "http://localhost:4873/json-schema/-/json-schema-0.4.0.tgz", @@ -8091,6 +10544,12 @@ "dev": true, "license": "MIT" }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "http://localhost:4873/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "license": "ISC" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "http://localhost:4873/json5/-/json5-2.2.3.tgz", @@ -8124,6 +10583,45 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsonpath-plus": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-7.2.0.tgz", + "integrity": "sha512-zBfiUPM5nD0YZSBT/o/fbCUlCcepMIdP0CJZxM1+KgA4f2T206f6VAg9e7mX35+KlMaIc5qXW34f3BnwJ3w+RA==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "http://localhost:4873/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "http://localhost:4873/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/kafkajs": { + "version": "2.2.4", + "resolved": "http://localhost:4873/kafkajs/-/kafkajs-2.2.4.tgz", + "integrity": "sha512-j/YeapB1vfPT2iOIUn/vxdyKEuhuY2PxMBvf5JWux6iSaukAccrMtXEY/Lb7OvavDhOWME589bpLrEdnVHjfjA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "http://localhost:4873/keyv/-/keyv-4.5.4.tgz", @@ -8165,6 +10663,21 @@ "dev": true, "license": "MIT" }, + "node_modules/livekit-server-sdk": { + "version": "2.15.0", + "resolved": "http://localhost:4873/livekit-server-sdk/-/livekit-server-sdk-2.15.0.tgz", + "integrity": "sha512-HmzjWnwEwwShu8yUf7VGFXdc+BuMJR5pnIY4qsdlhqI9d9wDgq+4cdTEHg0NEBaiGnc6PCOBiaTYgmIyVJ0S9w==", + "license": "Apache-2.0", + "dependencies": { + "@bufbuild/protobuf": "^1.10.1", + "@livekit/protocol": "^1.43.1", + "camelcase-keys": "^9.0.0", + "jose": "^5.1.2" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/load-esm": { "version": "1.0.3", "resolved": "http://localhost:4873/load-esm/-/load-esm-1.0.3.tgz", @@ -8263,6 +10776,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "http://localhost:4873/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "http://localhost:4873/lru-cache/-/lru-cache-5.1.1.tgz", @@ -8316,6 +10835,30 @@ "tmpl": "1.0.5" } }, + "node_modules/map-obj": { + "version": "5.0.0", + "resolved": "http://localhost:4873/map-obj/-/map-obj-5.0.0.tgz", + "integrity": "sha512-2L3MIgJynYrZ3TYMriLDLWocz15okFakV6J12HXvMXDHui2x/zgChzg1u9mFFGbbGWE+GsLpQByt4POb9Or+uA==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/matcher": { + "version": "3.0.0", + "resolved": "http://localhost:4873/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "http://localhost:4873/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -8468,7 +11011,6 @@ "version": "1.2.8", "resolved": "http://localhost:4873/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -8478,12 +11020,29 @@ "version": "7.1.3", "resolved": "http://localhost:4873/minipass/-/minipass-7.1.3.tgz", "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", - "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=16 || 14 >=14.17" } }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "http://localhost:4873/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/module-details-from-path": { + "version": "1.0.4", + "resolved": "http://localhost:4873/module-details-from-path/-/module-details-from-path-1.0.4.tgz", + "integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "http://localhost:4873/ms/-/ms-2.1.3.tgz", @@ -8608,6 +11167,26 @@ "dev": true, "license": "MIT" }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "http://localhost:4873/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, "node_modules/node-emoji": { "version": "1.11.0", "resolved": "http://localhost:4873/node-emoji/-/node-emoji-1.11.0.tgz", @@ -8618,6 +11197,30 @@ "lodash": "^4.17.21" } }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "http://localhost:4873/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "http://localhost:4873/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "license": "MIT" + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "http://localhost:4873/node-int64/-/node-int64-0.4.0.tgz", @@ -8685,6 +11288,35 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "http://localhost:4873/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ofetch": { + "version": "1.5.1", + "resolved": "http://localhost:4873/ofetch/-/ofetch-1.5.1.tgz", + "integrity": "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==", + "license": "MIT", + "dependencies": { + "destr": "^2.0.5", + "node-fetch-native": "^1.6.7", + "ufo": "^1.6.1" + } + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "http://localhost:4873/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "http://localhost:4873/on-finished/-/on-finished-2.4.1.tgz", @@ -8722,6 +11354,50 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/onnxruntime-common": { + "version": "1.21.0", + "resolved": "http://localhost:4873/onnxruntime-common/-/onnxruntime-common-1.21.0.tgz", + "integrity": "sha512-Q632iLLrtCAVOTO65dh2+mNbQir/QNTVBG3h/QdZBpns7mZ0RYbLRBgGABPbpU9351AgYy7SJf1WaeVwMrBFPQ==", + "license": "MIT" + }, + "node_modules/onnxruntime-node": { + "version": "1.21.0", + "resolved": "http://localhost:4873/onnxruntime-node/-/onnxruntime-node-1.21.0.tgz", + "integrity": "sha512-NeaCX6WW2L8cRCSqy3bInlo5ojjQqu2fD3D+9W5qb5irwxhEyWKXeH2vZ8W9r6VxaMPUan+4/7NDwZMtouZxEw==", + "hasInstallScript": true, + "license": "MIT", + "os": [ + "win32", + "darwin", + "linux" + ], + "dependencies": { + "global-agent": "^3.0.0", + "onnxruntime-common": "1.21.0", + "tar": "^7.0.1" + } + }, + "node_modules/openai": { + "version": "6.32.0", + "resolved": "http://localhost:4873/openai/-/openai-6.32.0.tgz", + "integrity": "sha512-j3k+BjydAf8yQlcOI7WUQMQTbbF5GEIMAE2iZYCOzwwB3S2pCheaWYp+XZRNAch4jWVc52PMDGRRjutao3lLCg==", + "license": "Apache-2.0", + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "http://localhost:4873/optionator/-/optionator-0.9.4.tgz", @@ -8796,6 +11472,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-retry": { + "version": "4.6.2", + "resolved": "http://localhost:4873/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "license": "MIT", + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/p-try": { "version": "2.2.0", "resolved": "http://localhost:4873/p-try/-/p-try-2.2.0.tgz", @@ -8884,11 +11573,16 @@ "node": ">=8" } }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "http://localhost:4873/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, "node_modules/path-scurry": { "version": "2.0.2", "resolved": "http://localhost:4873/path-scurry/-/path-scurry-2.0.2.tgz", "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", - "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^11.0.0", @@ -8905,7 +11599,6 @@ "version": "11.2.7", "resolved": "http://localhost:4873/lru-cache/-/lru-cache-11.2.7.tgz", "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", - "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" @@ -8951,6 +11644,179 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pidusage": { + "version": "4.0.1", + "resolved": "http://localhost:4873/pidusage/-/pidusage-4.0.1.tgz", + "integrity": "sha512-yCH2dtLHfEBnzlHUJymR/Z1nN2ePG3m392Mv8TFlTP1B0xkpMQNHAnfkY0n2tAi6ceKO6YWhxYfZ96V4vVkh/g==", + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/pino": { + "version": "8.21.0", + "resolved": "http://localhost:4873/pino/-/pino-8.21.0.tgz", + "integrity": "sha512-ip4qdzjkAyDDZklUaZkcRFb2iA118H9SgRh8yzTkSQK8HilsOJF7rSY8HoW5+I0M46AZgX/pxbprf2vvzQCE0Q==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0", + "fast-redact": "^3.1.1", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^1.2.0", + "pino-std-serializers": "^6.0.0", + "process-warning": "^3.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^3.7.0", + "thread-stream": "^2.6.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "1.2.0", + "resolved": "http://localhost:4873/pino-abstract-transport/-/pino-abstract-transport-1.2.0.tgz", + "integrity": "sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q==", + "license": "MIT", + "dependencies": { + "readable-stream": "^4.0.0", + "split2": "^4.0.0" + } + }, + "node_modules/pino-abstract-transport/node_modules/buffer": { + "version": "6.0.3", + "resolved": "http://localhost:4873/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/pino-abstract-transport/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "http://localhost:4873/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/pino-pretty": { + "version": "11.3.0", + "resolved": "http://localhost:4873/pino-pretty/-/pino-pretty-11.3.0.tgz", + "integrity": "sha512-oXwn7ICywaZPHmu3epHGU2oJX4nPmKvHvB/bwrJHlGcbEWaVcotkpyVHMKLKmiVryWYByNp0jpgAcXpFJDXJzA==", + "license": "MIT", + "dependencies": { + "colorette": "^2.0.7", + "dateformat": "^4.6.3", + "fast-copy": "^3.0.2", + "fast-safe-stringify": "^2.1.1", + "help-me": "^5.0.0", + "joycon": "^3.1.1", + "minimist": "^1.2.6", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pump": "^3.0.0", + "readable-stream": "^4.0.0", + "secure-json-parse": "^2.4.0", + "sonic-boom": "^4.0.1", + "strip-json-comments": "^3.1.1" + }, + "bin": { + "pino-pretty": "bin.js" + } + }, + "node_modules/pino-pretty/node_modules/buffer": { + "version": "6.0.3", + "resolved": "http://localhost:4873/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/pino-pretty/node_modules/pino-abstract-transport": { + "version": "2.0.0", + "resolved": "http://localhost:4873/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", + "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-pretty/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "http://localhost:4873/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/pino-pretty/node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "http://localhost:4873/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "6.2.2", + "resolved": "http://localhost:4873/pino-std-serializers/-/pino-std-serializers-6.2.2.tgz", + "integrity": "sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==", + "license": "MIT" + }, "node_modules/pirates": { "version": "4.0.7", "resolved": "http://localhost:4873/pirates/-/pirates-4.0.7.tgz", @@ -9107,6 +11973,45 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "http://localhost:4873/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-warning": { + "version": "3.0.0", + "resolved": "http://localhost:4873/process-warning/-/process-warning-3.0.0.tgz", + "integrity": "sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==", + "license": "MIT" + }, + "node_modules/protobufjs": { + "version": "7.5.4", + "resolved": "http://localhost:4873/protobufjs/-/protobufjs-7.5.4.tgz", + "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "http://localhost:4873/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -9126,6 +12031,16 @@ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "license": "MIT" }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "http://localhost:4873/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "http://localhost:4873/punycode/-/punycode-2.3.1.tgz", @@ -9168,6 +12083,24 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "http://localhost:4873/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, + "node_modules/quick-lru": { + "version": "6.1.2", + "resolved": "http://localhost:4873/quick-lru/-/quick-lru-6.1.2.tgz", + "integrity": "sha512-AAFUA5O1d83pIHEhJwWCq/RQcRukCkn/NSm2QsTEMle5f2hP0ChI2+3Xb051PZCkLryI/Ir1MVKviT2FIloaTQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "http://localhost:4873/range-parser/-/range-parser-1.2.1.tgz", @@ -9227,6 +12160,15 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "http://localhost:4873/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, "node_modules/redis-errors": { "version": "1.2.0", "resolved": "http://localhost:4873/redis-errors/-/redis-errors-1.2.0.tgz", @@ -9274,6 +12216,40 @@ "node": ">=0.10.0" } }, + "node_modules/require-in-the-middle": { + "version": "7.5.2", + "resolved": "http://localhost:4873/require-in-the-middle/-/require-in-the-middle-7.5.2.tgz", + "integrity": "sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "module-details-from-path": "^1.0.3", + "resolve": "^1.22.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "http://localhost:4873/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/resolve-cwd": { "version": "3.0.0", "resolved": "http://localhost:4873/resolve-cwd/-/resolve-cwd-3.0.0.tgz", @@ -9338,6 +12314,38 @@ "dev": true, "license": "ISC" }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "http://localhost:4873/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/roarr": { + "version": "2.15.4", + "resolved": "http://localhost:4873/roarr/-/roarr-2.15.4.tgz", + "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", + "license": "BSD-3-Clause", + "dependencies": { + "boolean": "^3.0.1", + "detect-node": "^2.0.4", + "globalthis": "^1.0.1", + "json-stringify-safe": "^5.0.1", + "semver-compare": "^1.0.0", + "sprintf-js": "^1.1.2" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/roarr/node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "http://localhost:4873/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "license": "BSD-3-Clause" + }, "node_modules/router": { "version": "2.2.0", "resolved": "http://localhost:4873/router/-/router-2.2.0.tgz", @@ -9383,6 +12391,15 @@ ], "license": "MIT" }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "http://localhost:4873/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "http://localhost:4873/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -9408,11 +12425,16 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/secure-json-parse": { + "version": "2.7.0", + "resolved": "http://localhost:4873/secure-json-parse/-/secure-json-parse-2.7.0.tgz", + "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==", + "license": "BSD-3-Clause" + }, "node_modules/semver": { "version": "7.7.4", "resolved": "http://localhost:4873/semver/-/semver-7.7.4.tgz", "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -9421,6 +12443,12 @@ "node": ">=10" } }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "http://localhost:4873/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "license": "MIT" + }, "node_modules/send": { "version": "1.2.1", "resolved": "http://localhost:4873/send/-/send-1.2.1.tgz", @@ -9447,6 +12475,33 @@ "url": "https://opencollective.com/express" } }, + "node_modules/serialize-error": { + "version": "7.0.1", + "resolved": "http://localhost:4873/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "license": "MIT", + "dependencies": { + "type-fest": "^0.13.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/serialize-error/node_modules/type-fest": { + "version": "0.13.1", + "resolved": "http://localhost:4873/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/serve-static": { "version": "2.2.1", "resolved": "http://localhost:4873/serve-static/-/serve-static-2.2.1.tgz", @@ -9472,6 +12527,50 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "http://localhost:4873/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "http://localhost:4873/shebang-command/-/shebang-command-2.0.0.tgz", @@ -9495,6 +12594,12 @@ "node": ">=8" } }, + "node_modules/shimmer": { + "version": "1.2.1", + "resolved": "http://localhost:4873/shimmer/-/shimmer-1.2.1.tgz", + "integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==", + "license": "BSD-2-Clause" + }, "node_modules/side-channel": { "version": "1.1.0", "resolved": "http://localhost:4873/side-channel/-/side-channel-1.1.0.tgz", @@ -9674,6 +12779,15 @@ "node": ">= 0.6" } }, + "node_modules/sonic-boom": { + "version": "3.8.1", + "resolved": "http://localhost:4873/sonic-boom/-/sonic-boom-3.8.1.tgz", + "integrity": "sha512-y4Z8LCDBuum+PBP3lSV7RHrXscqksve/bi0as7mhwVnBW+/wUqKT/2Kb7um8yqcFy0duYbbPxzt89Zy2nOCaxg==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, "node_modules/source-map": { "version": "0.7.4", "resolved": "http://localhost:4873/source-map/-/source-map-0.7.4.tgz", @@ -9705,6 +12819,15 @@ "node": ">=0.10.0" } }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "http://localhost:4873/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "http://localhost:4873/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -9863,7 +12986,6 @@ "version": "3.1.1", "resolved": "http://localhost:4873/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -9937,6 +13059,18 @@ "node": ">=8" } }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "http://localhost:4873/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/symbol-observable": { "version": "4.0.0", "resolved": "http://localhost:4873/symbol-observable/-/symbol-observable-4.0.0.tgz", @@ -9977,6 +13111,31 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/tar": { + "version": "7.5.13", + "resolved": "http://localhost:4873/tar/-/tar-7.5.13.tgz", + "integrity": "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "http://localhost:4873/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/terser": { "version": "5.46.1", "resolved": "http://localhost:4873/terser/-/terser-5.46.1.tgz", @@ -10180,6 +13339,15 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/thread-stream": { + "version": "2.7.0", + "resolved": "http://localhost:4873/thread-stream/-/thread-stream-2.7.0.tgz", + "integrity": "sha512-qQiRWsU/wvNolI6tbbCKd9iKaTnCXsTwVxhhKM6nctPdujTyztjlbUkUTUymidWcMnZ5pWR0ej4a0tjsW021vw==", + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "http://localhost:4873/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -10528,7 +13696,6 @@ "version": "5.9.3", "resolved": "http://localhost:4873/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -10562,6 +13729,12 @@ "typescript": ">=4.8.4 <6.0.0" } }, + "node_modules/ufo": { + "version": "1.6.3", + "resolved": "http://localhost:4873/ufo/-/ufo-1.6.3.tgz", + "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", + "license": "MIT" + }, "node_modules/uglify-js": { "version": "3.19.3", "resolved": "http://localhost:4873/uglify-js/-/uglify-js-3.19.3.tgz", @@ -10707,6 +13880,19 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/uuid": { + "version": "11.1.0", + "resolved": "http://localhost:4873/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "http://localhost:4873/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", @@ -10772,6 +13958,15 @@ "defaults": "^1.0.3" } }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "http://localhost:4873/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/webpack": { "version": "5.105.4", "resolved": "http://localhost:4873/webpack/-/webpack-5.105.4.tgz", @@ -11168,10 +14363,18 @@ "resolved": "http://localhost:4873/zod/-/zod-4.3.6.tgz", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.1", + "resolved": "http://localhost:4873/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz", + "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25 || ^4" + } } } } diff --git a/package.json b/package.json index 8b6f103..b85cedc 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,9 @@ "@ai-sdk/anthropic": "^3.0.58", "@ai-sdk/openai": "^3.0.41", "@deepgram/sdk": "^5.0.0", + "@livekit/agents": "^1.2.1", + "@livekit/agents-plugin-google": "^1.2.1", + "@livekit/agents-plugin-silero": "^1.2.1", "@nestjs/common": "^11.0.1", "@nestjs/config": "^4.0.3", "@nestjs/core": "^11.0.1", @@ -32,9 +35,12 @@ "ai": "^6.0.116", "axios": "^1.13.6", "ioredis": "^5.10.1", + "json-rules-engine": "^6.6.0", + "kafkajs": "^2.2.4", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", - "socket.io": "^4.8.3" + "socket.io": "^4.8.3", + "zod": "^4.3.6" }, "devDependencies": { "@eslint/eslintrc": "^3.2.0", diff --git a/src/ai/ai-chat.controller.ts b/src/ai/ai-chat.controller.ts index e3c9bd9..6964943 100644 --- a/src/ai/ai-chat.controller.ts +++ b/src/ai/ai-chat.controller.ts @@ -1,6 +1,7 @@ -import { Controller, Post, Body, Headers, HttpException, Logger } from '@nestjs/common'; +import { Controller, Post, Body, Headers, Req, Res, HttpException, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import { generateText, tool, stepCountIs } from 'ai'; +import type { Request, Response } from 'express'; +import { generateText, streamText, tool, stepCountIs } from 'ai'; import type { LanguageModel } from 'ai'; import { z } from 'zod'; import { PlatformGraphqlService } from '../platform/platform-graphql.service'; @@ -61,6 +62,432 @@ export class AiChatController { } } + @Post('stream') + async stream(@Req() req: Request, @Res() res: Response, @Headers('authorization') auth: string) { + if (!auth) throw new HttpException('Authorization required', 401); + + const body = req.body; + const messages = body.messages ?? []; + if (!messages.length) throw new HttpException('messages required', 400); + + if (!this.aiModel) { + res.status(500).json({ error: 'AI not configured' }); + return; + } + + const ctx = body.context; + let systemPrompt: string; + + // Rules engine context — use rules-specific system prompt + if (ctx?.type === 'rules-engine') { + systemPrompt = this.buildRulesSystemPrompt(ctx.currentConfig); + } else if (ctx?.type === 'supervisor') { + systemPrompt = this.buildSupervisorSystemPrompt(); + } else { + const kb = await this.buildKnowledgeBase(auth); + systemPrompt = this.buildSystemPrompt(kb); + + // Inject caller context so the AI knows who is selected + if (ctx) { + const parts: string[] = []; + if (ctx.leadName) parts.push(`Currently viewing/talking to: ${ctx.leadName}`); + if (ctx.callerPhone) parts.push(`Phone: ${ctx.callerPhone}`); + if (ctx.leadId) parts.push(`Lead ID: ${ctx.leadId}`); + if (parts.length) { + systemPrompt += `\n\nCURRENT CONTEXT:\n${parts.join('\n')}\nUse this context to answer questions about "this patient" or "this caller" without asking for their name.`; + } + } + } + + const platformService = this.platform; + const isSupervisor = ctx?.type === 'supervisor'; + + // Supervisor tools — agent performance, campaign stats, team metrics + const supervisorTools = { + get_agent_performance: tool({ + description: 'Get performance metrics for all agents or a specific agent. Returns call counts, conversion rates, idle time, NPS scores.', + inputSchema: z.object({ + agentName: z.string().optional().describe('Agent name to look up. Leave empty for all agents.'), + }), + execute: async ({ agentName }) => { + const [callsData, leadsData, agentsData, followUpsData] = await Promise.all([ + platformService.queryWithAuth( + `{ calls(first: 200, orderBy: [{ startedAt: DescNullsLast }]) { edges { node { id direction callStatus agentName startedAt durationSec disposition } } } }`, + undefined, auth, + ), + platformService.queryWithAuth( + `{ leads(first: 200) { edges { node { id assignedAgent status } } } }`, + undefined, auth, + ), + platformService.queryWithAuth( + `{ agents(first: 20) { edges { node { id name ozonetelagentid npsscore maxidleminutes minnpsthreshold minconversionpercent } } } }`, + undefined, auth, + ), + platformService.queryWithAuth( + `{ followUps(first: 100) { edges { node { id assignedAgent status } } } }`, + undefined, auth, + ), + ]); + + const calls = callsData.calls.edges.map((e: any) => e.node); + const leads = leadsData.leads.edges.map((e: any) => e.node); + const agents = agentsData.agents.edges.map((e: any) => e.node); + const followUps = followUpsData.followUps.edges.map((e: any) => e.node); + + const agentMetrics = agents + .filter((a: any) => !agentName || a.name.toLowerCase().includes(agentName.toLowerCase())) + .map((agent: any) => { + const agentCalls = calls.filter((c: any) => c.agentName === agent.name || c.agentName === agent.ozonetelagentid); + const totalCalls = agentCalls.length; + const missed = agentCalls.filter((c: any) => c.callStatus === 'MISSED').length; + const completed = agentCalls.filter((c: any) => c.callStatus === 'COMPLETED').length; + const apptBooked = agentCalls.filter((c: any) => c.disposition === 'APPOINTMENT_BOOKED').length; + const agentLeads = leads.filter((l: any) => l.assignedAgent === agent.name); + const agentFollowUps = followUps.filter((f: any) => f.assignedAgent === agent.name); + const pendingFollowUps = agentFollowUps.filter((f: any) => f.status === 'PENDING' || f.status === 'OVERDUE').length; + const conversionRate = totalCalls > 0 ? Math.round((apptBooked / totalCalls) * 100) : 0; + + return { + name: agent.name, + totalCalls, + completed, + missed, + appointmentsBooked: apptBooked, + conversionRate: `${conversionRate}%`, + assignedLeads: agentLeads.length, + pendingFollowUps, + npsScore: agent.npsscore, + maxIdleMinutes: agent.maxidleminutes, + minNpsThreshold: agent.minnpsthreshold, + minConversionPercent: agent.minconversionpercent, + belowNpsThreshold: agent.minnpsthreshold && (agent.npsscore ?? 100) < agent.minnpsthreshold, + belowConversionThreshold: agent.minconversionpercent && conversionRate < agent.minconversionpercent, + }; + }); + + return { agents: agentMetrics, totalAgents: agentMetrics.length }; + }, + }), + + get_campaign_stats: tool({ + description: 'Get campaign performance stats — lead counts, conversion rates, sources.', + inputSchema: z.object({}), + execute: async () => { + const [campaignsData, leadsData] = await Promise.all([ + platformService.queryWithAuth( + `{ campaigns(first: 20) { edges { node { id campaignName campaignStatus platform leadCount convertedCount budget { amountMicros } } } } }`, + undefined, auth, + ), + platformService.queryWithAuth( + `{ leads(first: 200) { edges { node { id campaignId status } } } }`, + undefined, auth, + ), + ]); + + const campaigns = campaignsData.campaigns.edges.map((e: any) => e.node); + const leads = leadsData.leads.edges.map((e: any) => e.node); + + return { + campaigns: campaigns.map((c: any) => { + const campaignLeads = leads.filter((l: any) => l.campaignId === c.id); + const converted = campaignLeads.filter((l: any) => l.status === 'CONVERTED' || l.status === 'APPOINTMENT_SET').length; + return { + name: c.campaignName, + status: c.campaignStatus, + platform: c.platform, + totalLeads: campaignLeads.length, + converted, + conversionRate: campaignLeads.length > 0 ? `${Math.round((converted / campaignLeads.length) * 100)}%` : '0%', + budget: c.budget ? `₹${c.budget.amountMicros / 1_000_000}` : null, + }; + }), + }; + }, + }), + + get_call_summary: tool({ + description: 'Get aggregate call statistics — total calls, inbound/outbound split, missed call rate, average duration, disposition breakdown.', + inputSchema: z.object({ + period: z.string().optional().describe('Period: "today", "week", "month". Defaults to "week".'), + }), + execute: async ({ period }) => { + const data = await platformService.queryWithAuth( + `{ calls(first: 500, orderBy: [{ startedAt: DescNullsLast }]) { edges { node { id direction callStatus agentName startedAt durationSec disposition } } } }`, + undefined, auth, + ); + const allCalls = data.calls.edges.map((e: any) => e.node); + + // Filter by period + const now = new Date(); + const start = new Date(now); + if (period === 'today') start.setHours(0, 0, 0, 0); + else if (period === 'month') start.setDate(start.getDate() - 30); + else start.setDate(start.getDate() - 7); // default week + + const calls = allCalls.filter((c: any) => c.startedAt && new Date(c.startedAt) >= start); + + const total = calls.length; + const inbound = calls.filter((c: any) => c.direction === 'INBOUND').length; + const outbound = total - inbound; + const missed = calls.filter((c: any) => c.callStatus === 'MISSED').length; + const completed = calls.filter((c: any) => c.callStatus === 'COMPLETED').length; + const totalDuration = calls.reduce((sum: number, c: any) => sum + (c.durationSec ?? 0), 0); + const avgDuration = completed > 0 ? Math.round(totalDuration / completed) : 0; + + const dispositions: Record = {}; + for (const c of calls) { + if (c.disposition) dispositions[c.disposition] = (dispositions[c.disposition] ?? 0) + 1; + } + + return { + period: period ?? 'week', + total, + inbound, + outbound, + missed, + completed, + missedRate: total > 0 ? `${Math.round((missed / total) * 100)}%` : '0%', + avgDurationSeconds: avgDuration, + dispositions, + }; + }, + }), + + get_sla_breaches: tool({ + description: 'Get calls/leads that have breached their SLA — items that were not handled within the expected timeframe.', + inputSchema: z.object({}), + execute: async () => { + const data = await platformService.queryWithAuth( + `{ calls(first: 100, filter: { callStatus: { eq: MISSED }, callbackstatus: { eq: PENDING_CALLBACK } }) { edges { node { id callerNumber { primaryPhoneNumber } startedAt agentName sla } } } }`, + undefined, auth, + ); + const breached = data.calls.edges + .map((e: any) => e.node) + .filter((c: any) => (c.sla ?? 0) > 100); + + return { + breachedCount: breached.length, + items: breached.map((c: any) => ({ + id: c.id, + phone: c.callerNumber?.primaryPhoneNumber ?? 'Unknown', + slaPercent: c.sla, + missedAt: c.startedAt, + agent: c.agentName, + })), + }; + }, + }), + }; + + // Agent tools — patient lookup, appointments, doctors + const agentTools = { + lookup_patient: tool({ + description: 'Search for a patient/lead by phone number or name. Returns their profile, lead status, AI summary.', + inputSchema: z.object({ + phone: z.string().optional().describe('Phone number to search'), + name: z.string().optional().describe('Patient/lead name to search'), + }), + execute: async ({ phone, name }) => { + const data = await platformService.queryWithAuth( + `{ leads(first: 50) { edges { node { + id name contactName { firstName lastName } + contactPhone { primaryPhoneNumber } + source status interestedService + contactAttempts lastContacted + aiSummary aiSuggestedAction patientId + } } } }`, + undefined, auth, + ); + const leads = data.leads.edges.map((e: any) => e.node); + const phoneClean = (phone ?? '').replace(/\D/g, ''); + const nameClean = (name ?? '').toLowerCase(); + + const matched = leads.filter((l: any) => { + if (phoneClean) { + const lp = (l.contactPhone?.primaryPhoneNumber ?? '').replace(/\D/g, ''); + if (lp.endsWith(phoneClean) || phoneClean.endsWith(lp)) return true; + } + if (nameClean) { + const fn = `${l.contactName?.firstName ?? ''} ${l.contactName?.lastName ?? ''}`.toLowerCase(); + if (fn.includes(nameClean)) return true; + } + return false; + }); + + if (!matched.length) return { found: false, message: 'No patient/lead found.' }; + return { found: true, count: matched.length, leads: matched }; + }, + }), + + lookup_appointments: tool({ + description: 'Get appointments for a patient. Returns doctor, department, date, status.', + inputSchema: z.object({ + patientId: z.string().describe('Patient ID'), + }), + execute: async ({ patientId }) => { + const data = await platformService.queryWithAuth( + `{ appointments(first: 20, filter: { patientId: { eq: "${patientId}" } }, orderBy: [{ scheduledAt: DescNullsLast }]) { edges { node { + id scheduledAt status doctorName department reasonForVisit + } } } }`, + undefined, auth, + ); + return { appointments: data.appointments.edges.map((e: any) => e.node) }; + }, + }), + + lookup_doctor: tool({ + description: 'Get doctor details — schedule, clinic, fees, specialty.', + inputSchema: z.object({ + doctorName: z.string().describe('Doctor name'), + }), + execute: async ({ doctorName }) => { + const data = await platformService.queryWithAuth( + `{ doctors(first: 10) { edges { node { + id fullName { firstName lastName } + department specialty visitingHours + consultationFeeNew { amountMicros currencyCode } + clinic { clinicName } + } } } }`, + undefined, auth, + ); + const doctors = data.doctors.edges.map((e: any) => e.node); + // Strip "Dr." prefix and search flexibly + const search = doctorName.toLowerCase().replace(/^dr\.?\s*/i, '').trim(); + const searchWords = search.split(/\s+/); + const matched = doctors.filter((d: any) => { + const fn = (d.fullName?.firstName ?? '').toLowerCase(); + const ln = (d.fullName?.lastName ?? '').toLowerCase(); + const full = `${fn} ${ln}`; + return searchWords.some(w => w.length > 1 && (fn.includes(w) || ln.includes(w) || full.includes(w))); + }); + this.logger.log(`[TOOL] lookup_doctor: search="${doctorName}" → ${matched.length} results`); + if (!matched.length) return { found: false, message: `No doctor matching "${doctorName}". Available: ${doctors.map((d: any) => `Dr. ${d.fullName?.lastName ?? d.fullName?.firstName}`).join(', ')}` }; + return { found: true, doctors: matched }; + }, + }), + + book_appointment: tool({ + description: 'Book an appointment for a patient. Collect patient name, phone, department, doctor, preferred date/time, and reason before calling this.', + inputSchema: z.object({ + patientName: z.string().describe('Full name of the patient'), + phoneNumber: z.string().describe('Patient phone number'), + department: z.string().describe('Department for the appointment'), + doctorName: z.string().describe('Doctor name'), + scheduledAt: z.string().describe('Date and time in ISO format (e.g. 2026-04-01T10:00:00)'), + reason: z.string().describe('Reason for visit'), + }), + execute: async ({ patientName, phoneNumber, department, doctorName, scheduledAt, reason }) => { + this.logger.log(`[TOOL] book_appointment: ${patientName} | ${phoneNumber} | ${department} | ${doctorName} | ${scheduledAt}`); + try { + const result = await platformService.queryWithAuth( + `mutation($data: AppointmentCreateInput!) { createAppointment(data: $data) { id } }`, + { + data: { + name: `AI Booking — ${patientName} (${department})`, + scheduledAt, + status: 'SCHEDULED', + doctorName, + department, + reasonForVisit: reason, + }, + }, + auth, + ); + const id = result?.createAppointment?.id; + if (id) { + return { booked: true, appointmentId: id, message: `Appointment booked for ${patientName} with ${doctorName} on ${scheduledAt}. Reference: ${id.substring(0, 8)}` }; + } + return { booked: false, message: 'Appointment creation failed.' }; + } catch (err: any) { + this.logger.error(`[TOOL] book_appointment failed: ${err.message}`); + return { booked: false, message: `Failed to book: ${err.message}` }; + } + }, + }), + + create_lead: tool({ + description: 'Create a new lead/enquiry for a caller who is not an existing patient. Collect name, phone, and interest.', + inputSchema: z.object({ + name: z.string().describe('Caller name'), + phoneNumber: z.string().describe('Phone number'), + interest: z.string().describe('What they are enquiring about'), + }), + execute: async ({ name, phoneNumber, interest }) => { + this.logger.log(`[TOOL] create_lead: ${name} | ${phoneNumber} | ${interest}`); + try { + const cleanPhone = phoneNumber.replace(/[^0-9]/g, '').slice(-10); + const result = await platformService.queryWithAuth( + `mutation($data: LeadCreateInput!) { createLead(data: $data) { id } }`, + { + data: { + name: `AI Enquiry — ${name}`, + contactName: { + firstName: name.split(' ')[0], + lastName: name.split(' ').slice(1).join(' ') || '', + }, + contactPhone: { primaryPhoneNumber: `+91${cleanPhone}` }, + source: 'PHONE', + status: 'NEW', + interestedService: interest, + }, + }, + auth, + ); + const id = result?.createLead?.id; + if (id) { + return { created: true, leadId: id, message: `Lead created for ${name}. Our team will follow up on ${phoneNumber}.` }; + } + return { created: false, message: 'Lead creation failed.' }; + } catch (err: any) { + this.logger.error(`[TOOL] create_lead failed: ${err.message}`); + return { created: false, message: `Failed: ${err.message}` }; + } + }, + }), + + lookup_call_history: tool({ + description: 'Get call log for a lead — all inbound/outbound calls with dispositions and durations.', + inputSchema: z.object({ + leadId: z.string().describe('Lead ID'), + }), + execute: async ({ leadId }) => { + const data = await platformService.queryWithAuth( + `{ calls(first: 20, filter: { leadId: { eq: "${leadId}" } }, orderBy: [{ startedAt: DescNullsLast }]) { edges { node { + id direction callStatus agentName startedAt durationSec disposition + } } } }`, + undefined, auth, + ); + return { calls: data.calls.edges.map((e: any) => e.node) }; + }, + }), + }; + + const result = streamText({ + model: this.aiModel, + system: systemPrompt, + messages, + stopWhen: stepCountIs(5), + tools: isSupervisor ? supervisorTools : agentTools, + }); + + const response = result.toTextStreamResponse(); + res.status(response.status); + response.headers.forEach((value, key) => res.setHeader(key, value)); + if (response.body) { + const reader = response.body.getReader(); + const pump = async () => { + while (true) { + const { done, value } = await reader.read(); + if (done) { res.end(); break; } + res.write(value); + } + }; + pump().catch(() => res.end()); + } else { + res.end(); + } + } + private async buildKnowledgeBase(auth: string): Promise { const now = Date.now(); if (this.knowledgeBase && now - this.kbLoadedAt < this.kbTtlMs) { @@ -85,17 +512,18 @@ export class AiChatController { ); const clinics = clinicData.clinics.edges.map((e: any) => e.node); if (clinics.length) { - sections.push('## Clinics'); + sections.push('## CLINICS & TIMINGS'); for (const c of clinics) { + const name = c.clinicName ?? c.name; const addr = c.addressCustom ? [c.addressCustom.addressStreet1, c.addressCustom.addressCity].filter(Boolean).join(', ') : ''; - const hours = [ - c.weekdayHours ? `Mon–Fri ${c.weekdayHours}` : '', - c.saturdayHours ? `Sat ${c.saturdayHours}` : '', - c.sundayHours ? `Sun ${c.sundayHours}` : 'Sun closed', - ].filter(Boolean).join(', '); - sections.push(`- ${c.clinicName ?? c.name}: ${addr}. ${hours}.`); + sections.push(`### ${name}`); + if (addr) sections.push(` Address: ${addr}`); + if (c.weekdayHours) sections.push(` Mon–Fri: ${c.weekdayHours}`); + if (c.saturdayHours) sections.push(` Saturday: ${c.saturdayHours}`); + sections.push(` Sunday: ${c.sundayHours ?? 'Closed'}`); + if (c.walkInAllowed) sections.push(` Walk-ins: Accepted`); } const rulesClinic = clinics[0]; @@ -121,7 +549,36 @@ export class AiChatController { } } catch (err) { this.logger.warn(`Failed to fetch clinics: ${err}`); - sections.push('## Clinics\nFailed to load clinic data.'); + sections.push('## CLINICS\nFailed to load clinic data.'); + } + + // Add doctors to KB + try { + const docData = await this.platform.queryWithAuth( + `{ doctors(first: 20) { edges { node { + fullName { firstName lastName } department specialty visitingHours + consultationFeeNew { amountMicros currencyCode } + clinic { clinicName } + } } } }`, + undefined, auth, + ); + const doctors = docData.doctors.edges.map((e: any) => e.node); + if (doctors.length) { + sections.push('\n## DOCTORS'); + for (const d of doctors) { + const name = `Dr. ${d.fullName?.firstName ?? ''} ${d.fullName?.lastName ?? ''}`.trim(); + const fee = d.consultationFeeNew ? `₹${d.consultationFeeNew.amountMicros / 1_000_000}` : ''; + const clinic = d.clinic?.clinicName ?? ''; + sections.push(`### ${name}`); + sections.push(` Department: ${d.department ?? 'N/A'}`); + sections.push(` Specialty: ${d.specialty ?? 'N/A'}`); + if (d.visitingHours) sections.push(` Hours: ${d.visitingHours}`); + if (fee) sections.push(` Consultation fee: ${fee}`); + if (clinic) sections.push(` Clinic: ${clinic}`); + } + } + } catch (err) { + this.logger.warn(`Failed to fetch doctors for KB: ${err}`); } try { @@ -187,20 +644,92 @@ export class AiChatController { return this.knowledgeBase; } + private buildSupervisorSystemPrompt(): string { + return `You are an AI assistant for supervisors at Global Hospital's call center (Helix Engage). +You help supervisors monitor team performance, identify issues, and make data-driven decisions. + +## YOUR CAPABILITIES +You have access to tools that query real-time data: +- **Agent performance**: call counts, conversion rates, NPS scores, idle time, pending follow-ups +- **Campaign stats**: lead counts, conversion rates per campaign, platform breakdown +- **Call summary**: total calls, inbound/outbound split, missed call rate, disposition breakdown +- **SLA breaches**: missed calls that haven't been called back within the SLA threshold + +## RULES +1. ALWAYS use tools to fetch data before answering. NEVER guess or fabricate performance numbers. +2. Be specific — include actual numbers from the tool response, not vague qualifiers. +3. When comparing agents, use their configured thresholds (minConversionPercent, minNpsThreshold, maxIdleMinutes) and team averages. Let the data determine who is underperforming — do not assume. +4. Be concise — supervisors want quick answers. Use bullet points. +5. When recommending actions, ground them in the data returned by tools. +6. If asked about trends, use the call summary tool with different periods. +7. Do not use any agent name in a negative context unless the data explicitly supports it.`; + } + + private buildRulesSystemPrompt(currentConfig: any): string { + const configJson = JSON.stringify(currentConfig, null, 2); + return `You are an AI assistant helping a hospital supervisor configure the Rules Engine for their call center worklist. + +## YOUR ROLE +You help the supervisor understand and optimize priority scoring rules. You explain concepts clearly and make specific recommendations based on their current configuration. + +## SCORING FORMULA +finalScore = baseWeight × slaMultiplier × campaignMultiplier + +- **Base Weight** (0-10): Each task type (missed calls, follow-ups, campaign leads, 2nd/3rd attempts) has a configurable weight. Higher weight = higher priority in the worklist. +- **SLA Multiplier**: Time-based urgency curve. Formula: elapsed^1.6 (accelerates as SLA deadline approaches). Past SLA breach: 1.0 + (excess × 0.5). This means items get increasingly urgent as they approach their SLA deadline. +- **Campaign Multiplier**: (campaignWeight/10) × (sourceWeight/10). IVF(9) × WhatsApp(9) = 0.81. Health(7) × Instagram(5) = 0.35. +- **SLA Thresholds**: Each task type has an SLA in minutes. Missed calls default to 12h (720min), follow-ups to 1d (1440min), campaign leads to 2d (2880min). + +## SLA STATUS COLORS +- Green (low): < 50% SLA elapsed +- Amber (medium): 50-80% SLA elapsed +- Red (high): 80-100% SLA elapsed +- Dark red pulsing (critical): > 100% SLA elapsed (breached) + +## PRIORITY RULES vs AUTOMATION RULES +- **Priority Rules** (what the supervisor is configuring now): Determine worklist order. Computed in real-time at request time. No permanent data changes. +- **Automation Rules** (coming soon): Trigger durable actions — assign leads to agents, escalate SLA breaches to supervisors, update lead status automatically. These write back to entity fields and need a draft/publish workflow. + +## BEST PRACTICES FOR HOSPITAL CALL CENTERS +- Missed calls should have the highest weight (8-10) — these are patients who tried to reach you +- Follow-ups should be high (7-9) — you committed to calling them back +- Campaign leads vary by campaign value (5-8) +- SLA for missed calls: 4-12 hours (shorter = more responsive) +- SLA for follow-ups: 12-24 hours +- High-value campaigns (IVF, cancer screening): weight 8-9 +- General campaigns (health checkup): weight 5-7 +- WhatsApp/Phone leads convert better than social media → weight them higher + +## CURRENT CONFIGURATION +${configJson} + +## RULES +1. Be concise — under 100 words unless asked for detail +2. When recommending changes, be specific: "Set missed call weight to 9, SLA to 8 hours" +3. Explain WHY a change matters: "This ensures IVF patients get called within 4 hours" +4. Reference the scoring formula when explaining scores +5. If asked about automation rules, explain the concept and say it's coming soon`; + } + private buildSystemPrompt(kb: string): string { - return `You are an AI assistant for call center agents at a hospital. -You help agents answer questions about patients, doctors, appointments, and hospital services during live calls. + return `You are an AI assistant for call center agents at Global Hospital, Bangalore. +You help agents answer questions about patients, doctors, appointments, clinics, and hospital services during live calls. + +IMPORTANT — ANSWER FROM KNOWLEDGE BASE FIRST: +The knowledge base below contains REAL clinic locations, timings, doctor details, health packages, and insurance partners. +When asked about clinic timings, locations, doctor availability, packages, or insurance — ALWAYS check the knowledge base FIRST before saying you don't know. +Example: "What are the Koramangala timings?" → Look for "Koramangala" in the Clinics section below. RULES: -1. For patient-specific questions, you MUST use lookup tools. NEVER guess patient data. -2. For doctor schedule/fees, use the lookup_doctor tool to get real data from the system. -3. For hospital info (clinics, packages, insurance), use the knowledge base below. -4. If a tool returns no data, say "I couldn't find that in our system." +1. For patient-specific questions (history, appointments, calls), use the lookup tools. NEVER guess patient data. +2. For doctor details beyond what's in the KB, use the lookup_doctor tool. +3. For clinic info, timings, packages, insurance → answer directly from the knowledge base below. +4. If you truly cannot find the answer in the KB or via tools, say "I couldn't find that in our system." 5. Be concise — agents are on live calls. Under 100 words unless asked for detail. 6. NEVER give medical advice, diagnosis, or treatment recommendations. -7. NEVER share sensitive hospital data (revenue, salaries, internal policies). -8. Format with bullet points for easy scanning. +7. Format with bullet points for easy scanning. +KNOWLEDGE BASE (this is real data from our system): ${kb}`; } diff --git a/src/app.module.ts b/src/app.module.ts index 36aaada..369cf8c 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -13,7 +13,12 @@ import { WorklistModule } from './worklist/worklist.module'; import { CallAssistModule } from './call-assist/call-assist.module'; import { SearchModule } from './search/search.module'; import { SupervisorModule } from './supervisor/supervisor.module'; -import { EmbedModule } from './embed/embed.module'; +import { MaintModule } from './maint/maint.module'; +import { RecordingsModule } from './recordings/recordings.module'; +import { EventsModule } from './events/events.module'; +import { CallerResolutionModule } from './caller/caller-resolution.module'; +import { RulesEngineModule } from './rules-engine/rules-engine.module'; +import { ConfigThemeModule } from './config/config-theme.module'; @Module({ imports: [ @@ -33,7 +38,12 @@ import { EmbedModule } from './embed/embed.module'; CallAssistModule, SearchModule, SupervisorModule, - EmbedModule, + MaintModule, + RecordingsModule, + EventsModule, + CallerResolutionModule, + RulesEngineModule, + ConfigThemeModule, ], }) export class AppModule {} diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts index 7c931b6..6ba6486 100644 --- a/src/auth/auth.controller.ts +++ b/src/auth/auth.controller.ts @@ -116,55 +116,60 @@ export class AuthController { this.logger.log(`User ${body.email} logged in with role: ${appRole} (platform roles: ${roleLabels.join(', ')})`); - // Multi-agent: resolve agent config + session lock for CC agents + // Check if user has an Agent entity with SIP config — applies to ALL roles let agentConfigResponse: any = undefined; + const memberId = workspaceMember?.id; - if (appRole === 'cc-agent') { - const memberId = workspaceMember?.id; - if (!memberId) throw new HttpException('Workspace member not found', 400); - + if (memberId) { const agentConfig = await this.agentConfigService.getByMemberId(memberId); - if (!agentConfig) { + + if (agentConfig) { + // Agent entity found — set up SIP + Ozonetel + const clientIp = (req.headers['x-forwarded-for'] as string)?.split(',')[0]?.trim() ?? req.ip ?? 'unknown'; + const existingSession = await this.sessionService.getSession(agentConfig.ozonetelAgentId); + if (existingSession) { + this.logger.warn(`Duplicate login blocked for ${body.email} — session held by IP ${existingSession.ip} since ${existingSession.lockedAt}`); + throw new HttpException(`You are already logged in from another device (${existingSession.ip}). Please log out there first.`, 409); + } + + await this.sessionService.lockSession(agentConfig.ozonetelAgentId, memberId, clientIp); + + this.ozonetelAgent.refreshToken().catch(err => { + this.logger.warn(`Ozonetel token refresh on login failed: ${err.message}`); + }); + + const ozAgentPassword = process.env.OZONETEL_AGENT_PASSWORD ?? 'Test123$'; + this.ozonetelAgent.loginAgent({ + agentId: agentConfig.ozonetelAgentId, + password: ozAgentPassword, + phoneNumber: agentConfig.sipExtension, + mode: 'blended', + }).catch(err => { + this.logger.warn(`Ozonetel agent login failed (non-blocking): ${err.message}`); + }); + + agentConfigResponse = { + ozonetelAgentId: agentConfig.ozonetelAgentId, + sipExtension: agentConfig.sipExtension, + sipPassword: agentConfig.sipPassword, + sipUri: agentConfig.sipUri, + sipWsServer: agentConfig.sipWsServer, + campaignName: agentConfig.campaignName, + }; + + this.logger.log(`Agent ${body.email} → Ozonetel ${agentConfig.ozonetelAgentId} / SIP ${agentConfig.sipExtension}`); + } else if (appRole === 'cc-agent') { + // CC agent role but no Agent entity — block login throw new HttpException('Agent account not configured. Contact administrator.', 403); + } else { + this.logger.log(`User ${body.email} has no Agent entity — SIP disabled`); } + } - // Check for duplicate login — strict: one device only - const clientIp = (req.headers['x-forwarded-for'] as string)?.split(',')[0]?.trim() ?? req.ip ?? 'unknown'; - const existingSession = await this.sessionService.getSession(agentConfig.ozonetelAgentId); - if (existingSession) { - this.logger.warn(`Duplicate login blocked for ${body.email} — session held by IP ${existingSession.ip} since ${existingSession.lockedAt}`); - throw new HttpException(`You are already logged in from another device (${existingSession.ip}). Please log out there first.`, 409); - } - - // Lock session in Redis with IP - await this.sessionService.lockSession(agentConfig.ozonetelAgentId, memberId, clientIp); - - // Force-refresh Ozonetel API token on login - this.ozonetelAgent.refreshToken().catch(err => { - this.logger.warn(`Ozonetel token refresh on login failed: ${err.message}`); - }); - - // Login to Ozonetel with agent-specific credentials - const ozAgentPassword = process.env.OZONETEL_AGENT_PASSWORD ?? 'Test123$'; - this.ozonetelAgent.loginAgent({ - agentId: agentConfig.ozonetelAgentId, - password: ozAgentPassword, - phoneNumber: agentConfig.sipExtension, - mode: 'blended', - }).catch(err => { - this.logger.warn(`Ozonetel agent login failed (non-blocking): ${err.message}`); - }); - - agentConfigResponse = { - ozonetelAgentId: agentConfig.ozonetelAgentId, - sipExtension: agentConfig.sipExtension, - sipPassword: agentConfig.sipPassword, - sipUri: agentConfig.sipUri, - sipWsServer: agentConfig.sipWsServer, - campaignName: agentConfig.campaignName, - }; - - this.logger.log(`CC agent ${body.email} → Ozonetel ${agentConfig.ozonetelAgentId} / SIP ${agentConfig.sipExtension}`); + // Cache agent name for worklist resolution (avoids re-querying currentUser with user JWT) + const agentFullName = `${workspaceMember?.name?.firstName ?? ''} ${workspaceMember?.name?.lastName ?? ''}`.trim(); + if (agentFullName) { + await this.sessionService.setCache(`agent:name:${accessToken.slice(-16)}`, agentFullName, 86400); } return { diff --git a/src/auth/session.service.ts b/src/auth/session.service.ts index b695002..5d72bf2 100644 --- a/src/auth/session.service.ts +++ b/src/auth/session.service.ts @@ -50,4 +50,28 @@ export class SessionService implements OnModuleInit { async unlockSession(agentId: string): Promise { await this.redis.del(this.key(agentId)); } + + // Generic cache operations for any module + async getCache(key: string): Promise { + return this.redis.get(key); + } + + async setCache(key: string, value: string, ttlSeconds: number): Promise { + await this.redis.set(key, value, 'EX', ttlSeconds); + } + + async deleteCache(key: string): Promise { + await this.redis.del(key); + } + + async scanKeys(pattern: string): Promise { + const keys: string[] = []; + let cursor = '0'; + do { + const [next, batch] = await this.redis.scan(cursor, 'MATCH', pattern, 'COUNT', 100); + cursor = next; + keys.push(...batch); + } while (cursor !== '0'); + return keys; + } } diff --git a/src/call-events/call-events.gateway.ts b/src/call-events/call-events.gateway.ts index 1efdcf2..5e8138e 100644 --- a/src/call-events/call-events.gateway.ts +++ b/src/call-events/call-events.gateway.ts @@ -35,6 +35,20 @@ export class CallEventsGateway { this.server.to(room).emit('call:incoming', event); } + // Broadcast to supervisors when a new call record is created + broadcastCallCreated(callData: any) { + this.logger.log('Broadcasting call:created to supervisor room'); + this.server.to('supervisor').emit('call:created', callData); + } + + // Supervisor registers to receive real-time updates + @SubscribeMessage('supervisor:register') + handleSupervisorRegister(@ConnectedSocket() client: Socket) { + client.join('supervisor'); + this.logger.log(`Supervisor registered (socket: ${client.id})`); + client.emit('supervisor:registered', { room: 'supervisor' }); + } + // Agent registers when they open the Call Desk page @SubscribeMessage('agent:register') handleAgentRegister( diff --git a/src/call-events/call-events.service.ts b/src/call-events/call-events.service.ts index 2f2c787..b5263d0 100644 --- a/src/call-events/call-events.service.ts +++ b/src/call-events/call-events.service.ts @@ -167,7 +167,24 @@ export class CallEventsService { `Processing disposition: ${payload.disposition} for call ${payload.callSid}`, ); - // 1. Create Call record in platform + // 1. Compute SLA % if lead is linked + let sla: number | undefined; + if (payload.leadId && payload.startedAt) { + try { + const lead = await this.platform.findLeadById(payload.leadId); + if (lead?.createdAt) { + const leadCreated = new Date(lead.createdAt).getTime(); + const callStarted = new Date(payload.startedAt).getTime(); + const elapsedMin = Math.max(0, (callStarted - leadCreated) / 60000); + const slaThresholdMin = 1440; // Default 24h; missed calls use 720 but this is a completed call + sla = Math.round((elapsedMin / slaThresholdMin) * 100); + } + } catch { + // SLA computation is best-effort + } + } + + // 2. Create Call record in platform try { await this.platform.createCall({ callDirection: 'INBOUND', @@ -187,8 +204,11 @@ export class CallEventsService { disposition: payload.disposition, callNotes: payload.notes || undefined, leadId: payload.leadId || undefined, + sla, }); - this.logger.log(`Call record created for ${payload.callSid}`); + this.logger.log(`Call record created for ${payload.callSid} (SLA: ${sla ?? 'N/A'}%)`); + // Notify supervisors in real-time + this.gateway.broadcastCallCreated({ callSid: payload.callSid, agentName: payload.agentName, disposition: payload.disposition }); } catch (error) { this.logger.error(`Failed to create call record: ${error}`); } diff --git a/src/caller/caller-resolution.controller.ts b/src/caller/caller-resolution.controller.ts new file mode 100644 index 0000000..35f2487 --- /dev/null +++ b/src/caller/caller-resolution.controller.ts @@ -0,0 +1,36 @@ +import { Controller, Post, Body, Headers, HttpException, HttpStatus, Logger } from '@nestjs/common'; +import { CallerResolutionService } from './caller-resolution.service'; + +@Controller('api/caller') +export class CallerResolutionController { + private readonly logger = new Logger(CallerResolutionController.name); + + constructor(private readonly resolution: CallerResolutionService) {} + + @Post('resolve') + async resolve( + @Body('phone') phone: string, + @Headers('authorization') auth: string, + ) { + if (!phone) { + throw new HttpException('phone is required', HttpStatus.BAD_REQUEST); + } + if (!auth) { + throw new HttpException('Authorization header required', HttpStatus.UNAUTHORIZED); + } + + this.logger.log(`[RESOLVE] Resolving caller: ${phone}`); + const result = await this.resolution.resolve(phone, auth); + return result; + } + + @Post('invalidate') + async invalidate(@Body('phone') phone: string) { + if (!phone) { + throw new HttpException('phone is required', HttpStatus.BAD_REQUEST); + } + this.logger.log(`[RESOLVE] Invalidating cache for: ${phone}`); + await this.resolution.invalidate(phone); + return { status: 'ok' }; + } +} diff --git a/src/caller/caller-resolution.module.ts b/src/caller/caller-resolution.module.ts new file mode 100644 index 0000000..c167d64 --- /dev/null +++ b/src/caller/caller-resolution.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { PlatformModule } from '../platform/platform.module'; +import { AuthModule } from '../auth/auth.module'; +import { CallerResolutionController } from './caller-resolution.controller'; +import { CallerResolutionService } from './caller-resolution.service'; + +@Module({ + imports: [PlatformModule, AuthModule], + controllers: [CallerResolutionController], + providers: [CallerResolutionService], + exports: [CallerResolutionService], +}) +export class CallerResolutionModule {} diff --git a/src/caller/caller-resolution.service.ts b/src/caller/caller-resolution.service.ts new file mode 100644 index 0000000..719786b --- /dev/null +++ b/src/caller/caller-resolution.service.ts @@ -0,0 +1,216 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { PlatformGraphqlService } from '../platform/platform-graphql.service'; +import { SessionService } from '../auth/session.service'; + +const CACHE_TTL = 3600; // 1 hour +const CACHE_PREFIX = 'caller:'; + +export type ResolvedCaller = { + leadId: string; + patientId: string; + firstName: string; + lastName: string; + phone: string; + isNew: boolean; // true if we just created the lead+patient pair +}; + +@Injectable() +export class CallerResolutionService { + private readonly logger = new Logger(CallerResolutionService.name); + + constructor( + private readonly platform: PlatformGraphqlService, + private readonly cache: SessionService, + ) {} + + // Resolve a caller by phone number. Always returns a paired lead + patient. + async resolve(phone: string, auth: string): Promise { + const normalized = phone.replace(/\D/g, '').slice(-10); + if (normalized.length < 10) { + throw new Error(`Invalid phone number: ${phone}`); + } + + // 1. Check cache + const cached = await this.cache.getCache(`${CACHE_PREFIX}${normalized}`); + if (cached) { + this.logger.log(`[RESOLVE] Cache hit for ${normalized}`); + return JSON.parse(cached); + } + + // 2. Look up lead by phone + const lead = await this.findLeadByPhone(normalized, auth); + + // 3. Look up patient by phone + const patient = await this.findPatientByPhone(normalized, auth); + + let result: ResolvedCaller; + + if (lead && patient) { + // Both exist — link them if not already linked + if (!lead.patientId) { + await this.linkLeadToPatient(lead.id, patient.id, auth); + this.logger.log(`[RESOLVE] Linked existing lead ${lead.id} → patient ${patient.id}`); + } + result = { + leadId: lead.id, + patientId: patient.id, + firstName: lead.firstName || patient.firstName, + lastName: lead.lastName || patient.lastName, + phone: normalized, + isNew: false, + }; + } else if (lead && !patient) { + // Lead exists, no patient — create patient + const newPatient = await this.createPatient(lead.firstName, lead.lastName, normalized, auth); + await this.linkLeadToPatient(lead.id, newPatient.id, auth); + this.logger.log(`[RESOLVE] Created patient ${newPatient.id} for existing lead ${lead.id}`); + result = { + leadId: lead.id, + patientId: newPatient.id, + firstName: lead.firstName, + lastName: lead.lastName, + phone: normalized, + isNew: false, + }; + } else if (!lead && patient) { + // Patient exists, no lead — create lead + const newLead = await this.createLead(patient.firstName, patient.lastName, normalized, patient.id, auth); + this.logger.log(`[RESOLVE] Created lead ${newLead.id} for existing patient ${patient.id}`); + result = { + leadId: newLead.id, + patientId: patient.id, + firstName: patient.firstName, + lastName: patient.lastName, + phone: normalized, + isNew: false, + }; + } else { + // Neither exists — create both + const newPatient = await this.createPatient('', '', normalized, auth); + const newLead = await this.createLead('', '', normalized, newPatient.id, auth); + this.logger.log(`[RESOLVE] Created new lead ${newLead.id} + patient ${newPatient.id} for ${normalized}`); + result = { + leadId: newLead.id, + patientId: newPatient.id, + firstName: '', + lastName: '', + phone: normalized, + isNew: true, + }; + } + + // 4. Cache the result + await this.cache.setCache(`${CACHE_PREFIX}${normalized}`, JSON.stringify(result), CACHE_TTL); + + return result; + } + + // Invalidate cache for a phone number (call after updates) + async invalidate(phone: string): Promise { + const normalized = phone.replace(/\D/g, '').slice(-10); + await this.cache.setCache(`${CACHE_PREFIX}${normalized}`, '', 1); // expire immediately + } + + private async findLeadByPhone(phone10: string, auth: string): Promise<{ id: string; firstName: string; lastName: string; patientId: string | null } | null> { + try { + const data = await this.platform.queryWithAuth<{ leads: { edges: { node: any }[] } }>( + `{ leads(first: 200) { edges { node { + id + contactName { firstName lastName } + contactPhone { primaryPhoneNumber } + patientId + } } } }`, + undefined, + auth, + ); + + const match = data.leads.edges.find(e => { + const num = (e.node.contactPhone?.primaryPhoneNumber ?? '').replace(/\D/g, '').slice(-10); + return num.length >= 10 && num === phone10; + }); + + if (!match) return null; + + return { + id: match.node.id, + firstName: match.node.contactName?.firstName ?? '', + lastName: match.node.contactName?.lastName ?? '', + patientId: match.node.patientId || null, + }; + } catch (err: any) { + this.logger.warn(`[RESOLVE] Lead lookup failed: ${err.message}`); + return null; + } + } + + private async findPatientByPhone(phone10: string, auth: string): Promise<{ id: string; firstName: string; lastName: string } | null> { + try { + const data = await this.platform.queryWithAuth<{ patients: { edges: { node: any }[] } }>( + `{ patients(first: 200) { edges { node { + id + fullName { firstName lastName } + phones { primaryPhoneNumber } + } } } }`, + undefined, + auth, + ); + + const match = data.patients.edges.find(e => { + const num = (e.node.phones?.primaryPhoneNumber ?? '').replace(/\D/g, '').slice(-10); + return num.length >= 10 && num === phone10; + }); + + if (!match) return null; + + return { + id: match.node.id, + firstName: match.node.fullName?.firstName ?? '', + lastName: match.node.fullName?.lastName ?? '', + }; + } catch (err: any) { + this.logger.warn(`[RESOLVE] Patient lookup failed: ${err.message}`); + return null; + } + } + + private async createPatient(firstName: string, lastName: string, phone: string, auth: string): Promise<{ id: string }> { + const data = await this.platform.queryWithAuth<{ createPatient: { id: string } }>( + `mutation($data: PatientCreateInput!) { createPatient(data: $data) { id } }`, + { + data: { + fullName: { firstName: firstName || 'Unknown', lastName: lastName || '' }, + phones: { primaryPhoneNumber: `+91${phone}` }, + patientType: 'NEW', + }, + }, + auth, + ); + return data.createPatient; + } + + private async createLead(firstName: string, lastName: string, phone: string, patientId: string, auth: string): Promise<{ id: string }> { + const data = await this.platform.queryWithAuth<{ createLead: { id: string } }>( + `mutation($data: LeadCreateInput!) { createLead(data: $data) { id } }`, + { + data: { + name: `${firstName} ${lastName}`.trim() || 'Unknown Caller', + contactName: { firstName: firstName || 'Unknown', lastName: lastName || '' }, + contactPhone: { primaryPhoneNumber: `+91${phone}` }, + source: 'PHONE', + status: 'NEW', + patientId, + }, + }, + auth, + ); + return data.createLead; + } + + private async linkLeadToPatient(leadId: string, patientId: string, auth: string): Promise { + await this.platform.queryWithAuth( + `mutation($id: UUID!, $data: LeadUpdateInput!) { updateLead(id: $id, data: $data) { id } }`, + { id: leadId, data: { patientId } }, + auth, + ); + } +} diff --git a/src/config/config-theme.module.ts b/src/config/config-theme.module.ts new file mode 100644 index 0000000..e299fa9 --- /dev/null +++ b/src/config/config-theme.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { ThemeController } from './theme.controller'; +import { ThemeService } from './theme.service'; + +@Module({ + controllers: [ThemeController], + providers: [ThemeService], + exports: [ThemeService], +}) +export class ConfigThemeModule {} diff --git a/src/config/theme.controller.ts b/src/config/theme.controller.ts new file mode 100644 index 0000000..5298d6c --- /dev/null +++ b/src/config/theme.controller.ts @@ -0,0 +1,27 @@ +import { Controller, Get, Put, Post, Body, Logger } from '@nestjs/common'; +import { ThemeService } from './theme.service'; +import type { ThemeConfig } from './theme.defaults'; + +@Controller('api/config') +export class ThemeController { + private readonly logger = new Logger(ThemeController.name); + + constructor(private readonly theme: ThemeService) {} + + @Get('theme') + getTheme() { + return this.theme.getTheme(); + } + + @Put('theme') + updateTheme(@Body() body: Partial) { + this.logger.log('Theme update request'); + return this.theme.updateTheme(body); + } + + @Post('theme/reset') + resetTheme() { + this.logger.log('Theme reset request'); + return this.theme.resetTheme(); + } +} diff --git a/src/config/theme.defaults.ts b/src/config/theme.defaults.ts new file mode 100644 index 0000000..97c998c --- /dev/null +++ b/src/config/theme.defaults.ts @@ -0,0 +1,79 @@ +export type ThemeConfig = { + version?: number; + updatedAt?: string; + brand: { + name: string; + hospitalName: string; + logo: string; + favicon: string; + }; + colors: { + brand: Record; + }; + typography: { + body: string; + display: string; + }; + login: { + title: string; + subtitle: string; + showGoogleSignIn: boolean; + showForgotPassword: boolean; + poweredBy: { label: string; url: string }; + }; + sidebar: { + title: string; + subtitle: string; + }; + ai: { + quickActions: Array<{ label: string; prompt: string }>; + }; +}; + +export const DEFAULT_THEME: ThemeConfig = { + brand: { + name: 'Helix Engage', + hospitalName: 'Global Hospital', + logo: '/helix-logo.png', + favicon: '/favicon.ico', + }, + colors: { + brand: { + '25': 'rgb(239 246 255)', + '50': 'rgb(219 234 254)', + '100': 'rgb(191 219 254)', + '200': 'rgb(147 197 253)', + '300': 'rgb(96 165 250)', + '400': 'rgb(59 130 246)', + '500': 'rgb(37 99 235)', + '600': 'rgb(29 78 216)', + '700': 'rgb(30 64 175)', + '800': 'rgb(30 58 138)', + '900': 'rgb(23 37 84)', + '950': 'rgb(15 23 42)', + }, + }, + typography: { + body: "'Satoshi', var(--font-inter, 'Inter'), -apple-system, 'Segoe UI', Roboto, Arial, sans-serif", + display: "'General Sans', var(--font-inter, 'Inter'), -apple-system, 'Segoe UI', Roboto, Arial, sans-serif", + }, + login: { + title: 'Sign in to Helix Engage', + subtitle: 'Global Hospital', + showGoogleSignIn: true, + showForgotPassword: true, + poweredBy: { label: 'Powered by F0rty2.ai', url: 'https://f0rty2.ai' }, + }, + sidebar: { + title: 'Helix Engage', + subtitle: 'Global Hospital \u00b7 {role}', + }, + ai: { + quickActions: [ + { label: 'Doctor availability', prompt: 'What doctors are available and what are their visiting hours?' }, + { label: 'Clinic timings', prompt: 'What are the clinic locations and timings?' }, + { label: 'Patient history', prompt: "Can you summarize this patient's history?" }, + { label: 'Treatment packages', prompt: 'What treatment packages are available?' }, + ], + }, +}; diff --git a/src/config/theme.service.ts b/src/config/theme.service.ts new file mode 100644 index 0000000..17febf7 --- /dev/null +++ b/src/config/theme.service.ts @@ -0,0 +1,98 @@ +import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; +import { readFileSync, writeFileSync, existsSync, mkdirSync, copyFileSync } from 'fs'; +import { join, dirname } from 'path'; +import { DEFAULT_THEME, type ThemeConfig } from './theme.defaults'; + +const THEME_PATH = join(process.cwd(), 'data', 'theme.json'); +const BACKUP_DIR = join(process.cwd(), 'data', 'theme-backups'); + +@Injectable() +export class ThemeService implements OnModuleInit { + private readonly logger = new Logger(ThemeService.name); + private cached: ThemeConfig | null = null; + + onModuleInit() { + this.load(); + } + + getTheme(): ThemeConfig { + if (this.cached) return this.cached; + return this.load(); + } + + updateTheme(updates: Partial): ThemeConfig { + const current = this.getTheme(); + + const merged: ThemeConfig = { + brand: { ...current.brand, ...updates.brand }, + colors: { + brand: { ...current.colors.brand, ...updates.colors?.brand }, + }, + typography: { ...current.typography, ...updates.typography }, + login: { ...current.login, ...updates.login, poweredBy: { ...current.login.poweredBy, ...updates.login?.poweredBy } }, + sidebar: { ...current.sidebar, ...updates.sidebar }, + ai: { + quickActions: updates.ai?.quickActions ?? current.ai.quickActions, + }, + }; + + merged.version = (current.version ?? 0) + 1; + merged.updatedAt = new Date().toISOString(); + + this.backup(); + + const dir = dirname(THEME_PATH); + if (!existsSync(dir)) mkdirSync(dir, { recursive: true }); + writeFileSync(THEME_PATH, JSON.stringify(merged, null, 2), 'utf8'); + this.cached = merged; + + this.logger.log(`Theme updated to v${merged.version}`); + return merged; + } + + resetTheme(): ThemeConfig { + this.backup(); + const dir = dirname(THEME_PATH); + if (!existsSync(dir)) mkdirSync(dir, { recursive: true }); + writeFileSync(THEME_PATH, JSON.stringify(DEFAULT_THEME, null, 2), 'utf8'); + this.cached = DEFAULT_THEME; + this.logger.log('Theme reset to defaults'); + return DEFAULT_THEME; + } + + private load(): ThemeConfig { + try { + if (existsSync(THEME_PATH)) { + const raw = readFileSync(THEME_PATH, 'utf8'); + const parsed = JSON.parse(raw); + this.cached = { + brand: { ...DEFAULT_THEME.brand, ...parsed.brand }, + colors: { brand: { ...DEFAULT_THEME.colors.brand, ...parsed.colors?.brand } }, + typography: { ...DEFAULT_THEME.typography, ...parsed.typography }, + login: { ...DEFAULT_THEME.login, ...parsed.login, poweredBy: { ...DEFAULT_THEME.login.poweredBy, ...parsed.login?.poweredBy } }, + sidebar: { ...DEFAULT_THEME.sidebar, ...parsed.sidebar }, + ai: { quickActions: parsed.ai?.quickActions ?? DEFAULT_THEME.ai.quickActions }, + }; + this.logger.log('Theme loaded from file'); + return this.cached; + } + } catch (err) { + this.logger.warn(`Failed to load theme: ${err}`); + } + + this.cached = DEFAULT_THEME; + this.logger.log('Using default theme'); + return DEFAULT_THEME; + } + + private backup() { + try { + if (!existsSync(THEME_PATH)) return; + if (!existsSync(BACKUP_DIR)) mkdirSync(BACKUP_DIR, { recursive: true }); + const ts = new Date().toISOString().replace(/[:.]/g, '-'); + copyFileSync(THEME_PATH, join(BACKUP_DIR, `theme-${ts}.json`)); + } catch (err) { + this.logger.warn(`Backup failed: ${err}`); + } + } +} diff --git a/src/events/consumers/ai-insight.consumer.ts b/src/events/consumers/ai-insight.consumer.ts new file mode 100644 index 0000000..63cddff --- /dev/null +++ b/src/events/consumers/ai-insight.consumer.ts @@ -0,0 +1,119 @@ +import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { generateObject } from 'ai'; +import { z } from 'zod'; +import { EventBusService } from '../event-bus.service'; +import { Topics } from '../event-types'; +import type { CallCompletedEvent } from '../event-types'; +import { PlatformGraphqlService } from '../../platform/platform-graphql.service'; +import { createAiModel } from '../../ai/ai-provider'; +import type { LanguageModel } from 'ai'; + +@Injectable() +export class AiInsightConsumer implements OnModuleInit { + private readonly logger = new Logger(AiInsightConsumer.name); + private readonly aiModel: LanguageModel | null; + + constructor( + private eventBus: EventBusService, + private platform: PlatformGraphqlService, + private config: ConfigService, + ) { + this.aiModel = createAiModel(config); + } + + onModuleInit() { + this.eventBus.on(Topics.CALL_COMPLETED, (event: CallCompletedEvent) => this.handleCallCompleted(event)); + } + + private async handleCallCompleted(event: CallCompletedEvent): Promise { + if (!event.leadId) { + this.logger.debug('[AI-INSIGHT] No leadId — skipping'); + return; + } + + if (!this.aiModel) { + this.logger.debug('[AI-INSIGHT] No AI model configured — skipping'); + return; + } + + this.logger.log(`[AI-INSIGHT] Generating insight for lead ${event.leadId}`); + + try { + // Fetch lead + all activities + const data = await this.platform.query( + `{ leads(filter: { id: { eq: "${event.leadId}" } }) { edges { node { + id name contactName { firstName lastName } + status source interestedService + contactAttempts lastContacted + } } } }`, + ); + const lead = data?.leads?.edges?.[0]?.node; + if (!lead) return; + + const activityData = await this.platform.query( + `{ leadActivities(first: 20, filter: { leadId: { eq: "${event.leadId}" } }, orderBy: [{ occurredAt: DescNullsLast }]) { + edges { node { activityType summary occurredAt channel durationSec outcome } } + } }`, + ); + const activities = activityData?.leadActivities?.edges?.map((e: any) => e.node) ?? []; + + const leadName = lead.contactName + ? `${lead.contactName.firstName ?? ''} ${lead.contactName.lastName ?? ''}`.trim() + : lead.name ?? 'Unknown'; + + // Build context + const activitySummary = activities.map((a: any) => + `${a.activityType}: ${a.summary} (${a.occurredAt ?? 'unknown date'})`, + ).join('\n'); + + // Generate insight + const { object } = await generateObject({ + model: this.aiModel, + schema: z.object({ + summary: z.string().describe('2-3 sentence summary of this lead based on all their interactions'), + suggestedAction: z.string().describe('One clear next action for the agent'), + }), + system: `You are a CRM assistant for Global Hospital Bangalore. +Generate a brief, actionable insight about this lead based on their interaction history. +Be specific — reference actual dates, dispositions, and patterns. +If the lead has booked appointments, mention upcoming ones. +If they keep calling about the same thing, note the pattern.`, + prompt: `Lead: ${leadName} +Status: ${lead.status ?? 'Unknown'} +Source: ${lead.source ?? 'Unknown'} +Interested in: ${lead.interestedService ?? 'Not specified'} +Contact attempts: ${lead.contactAttempts ?? 0} +Last contacted: ${lead.lastContacted ?? 'Never'} + +Recent activity (newest first): +${activitySummary || 'No activity recorded'} + +Latest call: +- Direction: ${event.direction} +- Duration: ${event.durationSec}s +- Disposition: ${event.disposition} +- Notes: ${event.notes ?? 'None'}`, + maxOutputTokens: 200, + }); + + // Update lead with new AI insight + await this.platform.query( + `mutation($id: UUID!, $data: LeadUpdateInput!) { updateLead(id: $id, data: $data) { id } }`, + { + id: event.leadId, + data: { + aiSummary: object.summary, + aiSuggestedAction: object.suggestedAction, + lastContacted: new Date().toISOString(), + contactAttempts: (lead.contactAttempts ?? 0) + 1, + }, + }, + ); + + this.logger.log(`[AI-INSIGHT] Updated lead ${event.leadId}: "${object.summary.substring(0, 60)}..."`); + } catch (err: any) { + this.logger.error(`[AI-INSIGHT] Failed for lead ${event.leadId}: ${err.message}`); + } + } +} diff --git a/src/events/event-bus.service.ts b/src/events/event-bus.service.ts new file mode 100644 index 0000000..3903517 --- /dev/null +++ b/src/events/event-bus.service.ts @@ -0,0 +1,114 @@ +import { Injectable, Logger, OnModuleInit, OnModuleDestroy } from '@nestjs/common'; +import { Kafka, Producer, Consumer, EachMessagePayload } from 'kafkajs'; +import type { EventPayload } from './event-types'; + +type EventHandler = (payload: any) => Promise; + +@Injectable() +export class EventBusService implements OnModuleInit, OnModuleDestroy { + private readonly logger = new Logger(EventBusService.name); + private kafka: Kafka; + private producer: Producer; + private consumer: Consumer; + private handlers = new Map(); + private connected = false; + + constructor() { + const brokers = (process.env.KAFKA_BROKERS ?? 'localhost:9092').split(','); + this.kafka = new Kafka({ + clientId: 'helix-engage-sidecar', + brokers, + retry: { retries: 5, initialRetryTime: 1000 }, + logLevel: 1, // ERROR only + }); + this.producer = this.kafka.producer(); + this.consumer = this.kafka.consumer({ groupId: 'helix-engage-workers' }); + } + + async onModuleInit() { + try { + await this.producer.connect(); + await this.consumer.connect(); + this.connected = true; + this.logger.log('Event bus connected (Kafka/Redpanda)'); + + // Subscribe to all topics we have handlers for + // Handlers are registered by consumer modules during their onModuleInit + // We start consuming after a short delay to let all handlers register + setTimeout(() => this.startConsuming(), 2000); + } catch (err: any) { + this.logger.warn(`Event bus not available (${err.message}) — running without events`); + this.connected = false; + } + } + + async onModuleDestroy() { + if (this.connected) { + await this.consumer.disconnect().catch(() => {}); + await this.producer.disconnect().catch(() => {}); + } + } + + async emit(topic: string, payload: EventPayload): Promise { + if (!this.connected) { + this.logger.debug(`[EVENT] Skipped (not connected): ${topic}`); + return; + } + + try { + await this.producer.send({ + topic, + messages: [{ value: JSON.stringify(payload), timestamp: Date.now().toString() }], + }); + this.logger.log(`[EVENT] Emitted: ${topic}`); + } catch (err: any) { + this.logger.error(`[EVENT] Failed to emit ${topic}: ${err.message}`); + } + } + + on(topic: string, handler: EventHandler): void { + const existing = this.handlers.get(topic) ?? []; + existing.push(handler); + this.handlers.set(topic, existing); + this.logger.log(`[EVENT] Handler registered for: ${topic}`); + } + + private async startConsuming(): Promise { + if (!this.connected) return; + + const topics = Array.from(this.handlers.keys()); + if (topics.length === 0) { + this.logger.log('[EVENT] No handlers registered — skipping consumer'); + return; + } + + try { + for (const topic of topics) { + await this.consumer.subscribe({ topic, fromBeginning: false }); + } + + await this.consumer.run({ + eachMessage: async (payload: EachMessagePayload) => { + const { topic, message } = payload; + const handlers = this.handlers.get(topic) ?? []; + if (handlers.length === 0 || !message.value) return; + + try { + const data = JSON.parse(message.value.toString()); + for (const handler of handlers) { + await handler(data).catch(err => + this.logger.error(`[EVENT] Handler error on ${topic}: ${err.message}`), + ); + } + } catch (err: any) { + this.logger.error(`[EVENT] Parse error on ${topic}: ${err.message}`); + } + }, + }); + + this.logger.log(`[EVENT] Consuming: ${topics.join(', ')}`); + } catch (err: any) { + this.logger.error(`[EVENT] Consumer failed: ${err.message}`); + } + } +} diff --git a/src/events/event-types.ts b/src/events/event-types.ts new file mode 100644 index 0000000..ff93fc0 --- /dev/null +++ b/src/events/event-types.ts @@ -0,0 +1,36 @@ +// Event topic names +export const Topics = { + CALL_COMPLETED: 'call.completed', + CALL_MISSED: 'call.missed', + AGENT_STATE: 'agent.state', +} as const; + +// Event payloads +export type CallCompletedEvent = { + callId: string | null; + ucid: string; + agentId: string; + callerPhone: string; + direction: string; + durationSec: number; + disposition: string; + leadId: string | null; + notes: string | null; + timestamp: string; +}; + +export type CallMissedEvent = { + callId: string | null; + callerPhone: string; + leadId: string | null; + leadName: string | null; + timestamp: string; +}; + +export type AgentStateEvent = { + agentId: string; + state: string; + timestamp: string; +}; + +export type EventPayload = CallCompletedEvent | CallMissedEvent | AgentStateEvent; diff --git a/src/events/events.module.ts b/src/events/events.module.ts new file mode 100644 index 0000000..b612a79 --- /dev/null +++ b/src/events/events.module.ts @@ -0,0 +1,12 @@ +import { Module, Global } from '@nestjs/common'; +import { PlatformModule } from '../platform/platform.module'; +import { EventBusService } from './event-bus.service'; +import { AiInsightConsumer } from './consumers/ai-insight.consumer'; + +@Global() +@Module({ + imports: [PlatformModule], + providers: [EventBusService, AiInsightConsumer], + exports: [EventBusService], +}) +export class EventsModule {} diff --git a/src/livekit-agent/agent.ts b/src/livekit-agent/agent.ts new file mode 100644 index 0000000..d62ce5f --- /dev/null +++ b/src/livekit-agent/agent.ts @@ -0,0 +1,279 @@ +import { WorkerOptions, defineAgent, llm, voice, VAD } from '@livekit/agents'; +import * as google from '@livekit/agents-plugin-google'; +import * as silero from '@livekit/agents-plugin-silero'; +import { z } from 'zod'; + +// Platform GraphQL helper +const SIDECAR_URL = process.env.SIDECAR_URL ?? 'http://localhost:4100'; +const PLATFORM_API_KEY = process.env.PLATFORM_API_KEY ?? ''; + +async function gql(query: string, variables?: Record): Promise { + if (!PLATFORM_API_KEY) return null; + try { + const res = await fetch(`${SIDECAR_URL}/graphql`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${PLATFORM_API_KEY}` }, + body: JSON.stringify({ query, variables }), + }); + const data = await res.json(); + if (data.errors) { + console.error('[AGENT-GQL] Error:', data.errors[0]?.message); + return null; + } + return data.data; + } catch (err) { + console.error('[AGENT-GQL] Failed:', err); + return null; + } +} + +// Hospital context — loaded on startup +let hospitalContext = { + doctors: [] as Array<{ name: string; department: string; specialty: string; id: string }>, + departments: [] as string[], +}; + +async function loadHospitalContext() { + const data = await gql(`{ doctors(first: 20) { edges { node { id fullName { firstName lastName } department specialty } } } }`); + if (data?.doctors?.edges) { + hospitalContext.doctors = data.doctors.edges.map((e: any) => ({ + id: e.node.id, + name: `Dr. ${e.node.fullName?.firstName ?? ''} ${e.node.fullName?.lastName ?? ''}`.trim(), + department: e.node.department ?? '', + specialty: e.node.specialty ?? '', + })); + hospitalContext.departments = [...new Set(hospitalContext.doctors.map(d => d.department))] as string[]; + console.log(`[LIVEKIT-AGENT] Loaded ${hospitalContext.doctors.length} doctors, ${hospitalContext.departments.length} departments`); + } else { + // Fallback + hospitalContext.doctors = [ + { id: '', name: 'Dr. Arun Sharma', department: 'Cardiology', specialty: 'Interventional Cardiology' }, + { id: '', name: 'Dr. Rajesh Kumar', department: 'Orthopedics', specialty: 'Joint Replacement' }, + { id: '', name: 'Dr. Meena Patel', department: 'Gynecology', specialty: 'Reproductive Medicine' }, + { id: '', name: 'Dr. Lakshmi Reddy', department: 'General Medicine', specialty: 'Internal Medicine' }, + { id: '', name: 'Dr. Harpreet Singh', department: 'ENT', specialty: 'Head & Neck Surgery' }, + ]; + hospitalContext.departments = ['Cardiology', 'Orthopedics', 'Gynecology', 'General Medicine', 'ENT']; + console.log('[LIVEKIT-AGENT] Using fallback doctor list'); + } +} + +// ─── Tools ──────────────────────────────────────────────────────────── + +const lookupDoctor = llm.tool({ + description: 'Look up available doctors by department or specialty. Call this when the patient asks about a specific department or type of doctor.', + parameters: z.object({ + department: z.string().nullable().describe('Department name like Cardiology, Orthopedics, ENT'), + specialty: z.string().nullable().describe('Specialty or condition like joint pain, heart, ear'), + }), + execute: async ({ department, specialty }) => { + let results = hospitalContext.doctors; + if (department) { + results = results.filter(d => d.department.toLowerCase().includes(department.toLowerCase())); + } + if (specialty) { + results = results.filter(d => + d.specialty.toLowerCase().includes(specialty.toLowerCase()) || + d.department.toLowerCase().includes(specialty.toLowerCase()), + ); + } + if (results.length === 0) return 'No matching doctors found. Available departments: ' + hospitalContext.departments.join(', '); + return results.map(d => `${d.name} — ${d.department} (${d.specialty})`).join('\n'); + }, +}); + +const bookAppointment = llm.tool({ + description: 'Book an appointment for the caller. You MUST collect patient name, phone number, department, preferred date/time, and reason before calling this.', + parameters: z.object({ + patientName: z.string().describe('Full name of the patient'), + phoneNumber: z.string().describe('Patient phone number with country code'), + department: z.string().describe('Department for the appointment'), + doctorName: z.string().nullable().describe('Preferred doctor name if specified'), + preferredDate: z.string().describe('Date in YYYY-MM-DD format or natural language'), + preferredTime: z.string().describe('Time slot like 10:00 AM, morning, afternoon'), + reason: z.string().describe('Reason for visit'), + }), + execute: async ({ patientName, phoneNumber, department, doctorName, preferredDate, preferredTime, reason }) => { + console.log(`[LIVEKIT-AGENT] Booking: ${patientName} | ${phoneNumber} | ${department} | ${doctorName ?? 'any'} | ${preferredDate} ${preferredTime}`); + + // Parse date — try ISO format first, fallback to tomorrow + let scheduledAt: string; + try { + const parsed = new Date(preferredDate); + if (!isNaN(parsed.getTime())) { + // Map time to hour + const timeMap: Record = { morning: '10:00', afternoon: '14:00', evening: '17:00' }; + const timeStr = timeMap[preferredTime.toLowerCase()] ?? preferredTime.replace(/\s*(AM|PM)/i, (_, p) => ''); + scheduledAt = new Date(`${parsed.toISOString().split('T')[0]}T${timeStr}:00`).toISOString(); + } else { + scheduledAt = new Date(Date.now() + 86400000).toISOString(); // tomorrow + } + } catch { + scheduledAt = new Date(Date.now() + 86400000).toISOString(); + } + + // Find matching doctor + const doctor = doctorName + ? hospitalContext.doctors.find(d => d.name.toLowerCase().includes(doctorName.toLowerCase())) + : hospitalContext.doctors.find(d => d.department.toLowerCase().includes(department.toLowerCase())); + + // Create appointment on platform + const result = await gql( + `mutation($data: AppointmentCreateInput!) { createAppointment(data: $data) { id } }`, + { + data: { + name: `AI Booking — ${patientName} (${department})`, + scheduledAt, + status: 'SCHEDULED', + doctorName: doctor?.name ?? doctorName ?? 'To be assigned', + department, + reasonForVisit: reason, + }, + }, + ); + + // Create or find lead + const cleanPhone = phoneNumber.replace(/[^0-9]/g, '').slice(-10); + await gql( + `mutation($data: LeadCreateInput!) { createLead(data: $data) { id } }`, + { + data: { + name: `AI — ${patientName}`, + contactName: { + firstName: patientName.split(' ')[0], + lastName: patientName.split(' ').slice(1).join(' ') || '', + }, + contactPhone: { primaryPhoneNumber: `+91${cleanPhone}` }, + source: 'PHONE', + status: 'APPOINTMENT_SET', + interestedService: department, + }, + }, + ); + + const refNum = `GH-${Date.now().toString().slice(-6)}`; + if (result?.createAppointment?.id) { + console.log(`[LIVEKIT-AGENT] Appointment created: ${result.createAppointment.id}`); + return `Appointment booked successfully! Reference number ${refNum}. ${patientName} is scheduled for ${department} on ${preferredDate} at ${preferredTime} with ${doctor?.name ?? 'an available doctor'}. A confirmation SMS will be sent to ${phoneNumber}.`; + } + return `I have noted the appointment request. Reference number ${refNum}. Our team will confirm the booking and send an SMS to ${phoneNumber}.`; + }, +}); + +const collectLeadInfo = llm.tool({ + description: 'Save the caller as a lead/enquiry when they are interested but not ready to book. Collect their name and phone number.', + parameters: z.object({ + name: z.string().describe('Caller name'), + phoneNumber: z.string().describe('Caller phone number'), + interest: z.string().describe('What they are interested in or enquiring about'), + }), + execute: async ({ name, phoneNumber, interest }) => { + console.log(`[LIVEKIT-AGENT] Lead: ${name} | ${phoneNumber} | ${interest}`); + + const cleanPhone = phoneNumber.replace(/[^0-9]/g, '').slice(-10); + const result = await gql( + `mutation($data: LeadCreateInput!) { createLead(data: $data) { id } }`, + { + data: { + name: `AI Enquiry — ${name}`, + contactName: { + firstName: name.split(' ')[0], + lastName: name.split(' ').slice(1).join(' ') || '', + }, + contactPhone: { primaryPhoneNumber: `+91${cleanPhone}` }, + source: 'PHONE', + status: 'NEW', + interestedService: interest, + }, + }, + ); + + if (result?.createLead?.id) { + console.log(`[LIVEKIT-AGENT] Lead created: ${result.createLead.id}`); + } + return `Thank you ${name}. I have noted your enquiry about ${interest}. One of our team members will call you back on ${phoneNumber} shortly.`; + }, +}); + +const transferToAgent = llm.tool({ + description: 'Transfer the call to a human agent. Use this when the caller explicitly asks to speak with a person, or when the query is too complex.', + parameters: z.object({ + reason: z.string().describe('Why the caller needs a human agent'), + }), + execute: async ({ reason }) => { + console.log(`[LIVEKIT-AGENT] Transfer requested: ${reason}`); + // TODO: When SIP is connected, trigger Ozonetel transfer via sidecar API + return 'I am transferring you to one of our agents now. Please hold for a moment. If no agent is available, someone will call you back within 15 minutes.'; + }, +}); + +// ─── Agent ──────────────────────────────────────────────────────────── + +const hospitalAgent = new voice.Agent({ + instructions: `You are the AI receptionist for Global Hospital, Bangalore. Your name is Helix. + +PERSONALITY: +- Warm, professional, and empathetic +- Speak clearly and at a moderate pace +- Use simple language — many callers may not be fluent in English +- Be concise — this is a phone call, not a chat +- Respond in the same language the caller uses (English, Hindi, Kannada) + +CAPABILITIES: +- Answer questions about hospital departments, doctors, and specialties +- Book appointments — collect: name, phone, department, preferred date/time, reason +- Take messages and create enquiries for callback +- Transfer to a human agent when needed + +HOSPITAL INFO: +- Global Hospital, Bangalore +- Open Monday to Saturday, 8 AM to 8 PM +- Emergency services available 24/7 +- Departments: ${hospitalContext.departments.join(', ') || 'Cardiology, Orthopedics, Gynecology, General Medicine, ENT'} + +RULES: +- Greet: "Hello, thank you for calling Global Hospital. This is Helix, how may I help you today?" +- If caller asks about pricing, say you will have the team call back with details +- Never give medical advice — always recommend consulting a doctor +- If the caller is in an emergency, tell them to visit the ER immediately or call 108 +- Always confirm all details before booking an appointment +- End calls politely: "Thank you for calling Global Hospital. Have a good day!" +- If you cannot understand the caller, politely ask them to repeat`, + llm: new google.beta.realtime.RealtimeModel({ + model: 'gemini-2.5-flash-native-audio-latest', + voice: 'Aoede', + temperature: 0.7, + }), + tools: { lookupDoctor, bookAppointment, collectLeadInfo, transferToAgent }, +}); + +// ─── Entry Point ────────────────────────────────────────────────────── + +export default defineAgent({ + prewarm: async (proc) => { + proc.userData.vad = await silero.VAD.load(); + await loadHospitalContext(); + }, + entry: async (ctx) => { + await ctx.connect(); + console.log(`[LIVEKIT-AGENT] Connected to room: ${ctx.room.name}`); + + const session = new voice.AgentSession({ + vad: ctx.proc.userData.vad as VAD, + }); + + await session.start({ agent: hospitalAgent, room: ctx.room }); + console.log('[LIVEKIT-AGENT] Voice session started'); + + // Gemini Realtime handles greeting via instructions — no separate say() needed + }, +}); + +// CLI runner +if (require.main === module) { + const options = new WorkerOptions({ + agent: __filename, + }); + const { cli } = require('@livekit/agents'); + cli.runApp(options); +} diff --git a/src/maint/maint.controller.ts b/src/maint/maint.controller.ts new file mode 100644 index 0000000..90c68a7 --- /dev/null +++ b/src/maint/maint.controller.ts @@ -0,0 +1,315 @@ +import { Controller, Post, UseGuards, Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { MaintGuard } from './maint.guard'; +import { OzonetelAgentService } from '../ozonetel/ozonetel-agent.service'; +import { PlatformGraphqlService } from '../platform/platform-graphql.service'; +import { SessionService } from '../auth/session.service'; +import { SupervisorService } from '../supervisor/supervisor.service'; +import { CallerResolutionService } from '../caller/caller-resolution.service'; + +@Controller('api/maint') +@UseGuards(MaintGuard) +export class MaintController { + private readonly logger = new Logger(MaintController.name); + + constructor( + private readonly config: ConfigService, + private readonly ozonetel: OzonetelAgentService, + private readonly platform: PlatformGraphqlService, + private readonly session: SessionService, + private readonly supervisor: SupervisorService, + private readonly callerResolution: CallerResolutionService, + ) {} + + @Post('force-ready') + async forceReady() { + const agentId = this.config.get('OZONETEL_AGENT_ID') ?? 'agent3'; + const password = process.env.OZONETEL_AGENT_PASSWORD ?? 'Test123$'; + const sipId = this.config.get('OZONETEL_SIP_ID') ?? '521814'; + + this.logger.log(`[MAINT] Force ready: agent=${agentId}`); + + try { + await this.ozonetel.logoutAgent({ agentId, password }); + const result = await this.ozonetel.loginAgent({ + agentId, + password, + phoneNumber: sipId, + mode: 'blended', + }); + this.logger.log(`[MAINT] Force ready complete: ${JSON.stringify(result)}`); + return { status: 'ok', message: `Agent ${agentId} force-readied`, result }; + } catch (error: any) { + const message = error.response?.data?.message ?? error.message ?? 'Force ready failed'; + this.logger.error(`[MAINT] Force ready failed: ${message}`); + return { status: 'error', message }; + } + } + + @Post('unlock-agent') + async unlockAgent() { + const agentId = this.config.get('OZONETEL_AGENT_ID') ?? 'agent3'; + this.logger.log(`[MAINT] Unlock agent session: ${agentId}`); + + try { + const existing = await this.session.getSession(agentId); + if (!existing) { + return { status: 'ok', message: `No active session for ${agentId}` }; + } + + await this.session.unlockSession(agentId); + + // Push force-logout via SSE to all connected browsers for this agent + this.supervisor.emitForceLogout(agentId); + + this.logger.log(`[MAINT] Session unlocked + force-logout pushed for ${agentId} (was held by IP ${existing.ip} since ${existing.lockedAt})`); + return { status: 'ok', message: `Session unlocked and force-logout sent for ${agentId}`, previousSession: existing }; + } catch (error: any) { + this.logger.error(`[MAINT] Unlock failed: ${error.message}`); + return { status: 'error', message: error.message }; + } + } + + @Post('backfill-missed-calls') + async backfillMissedCalls() { + this.logger.log('[MAINT] Backfill missed call lead names — starting'); + + // Fetch all missed calls without a leadId + const result = await this.platform.query( + `{ calls(first: 200, filter: { + callStatus: { eq: MISSED }, + leadId: { is: NULL } + }) { edges { node { id callerNumber { primaryPhoneNumber } } } } }`, + ); + + const calls = result?.calls?.edges?.map((e: any) => e.node) ?? []; + if (calls.length === 0) { + this.logger.log('[MAINT] No missed calls without leadId found'); + return { status: 'ok', total: 0, patched: 0 }; + } + + this.logger.log(`[MAINT] Found ${calls.length} missed calls without leadId`); + + let patched = 0; + let skipped = 0; + + for (const call of calls) { + const phone = call.callerNumber?.primaryPhoneNumber; + if (!phone) { skipped++; continue; } + + const phoneDigits = phone.replace(/^\+91/, ''); + try { + const leadResult = await this.platform.query( + `{ leads(first: 1, filter: { + contactPhone: { primaryPhoneNumber: { like: "%${phoneDigits}" } } + }) { edges { node { id contactName { firstName lastName } } } } }`, + ); + + const lead = leadResult?.leads?.edges?.[0]?.node; + if (!lead) { skipped++; continue; } + + const fn = lead.contactName?.firstName ?? ''; + const ln = lead.contactName?.lastName ?? ''; + const leadName = `${fn} ${ln}`.trim(); + + await this.platform.query( + `mutation { updateCall(id: "${call.id}", data: { + leadId: "${lead.id}"${leadName ? `, leadName: "${leadName}"` : ''} + }) { id } }`, + ); + + patched++; + this.logger.log(`[MAINT] Patched ${phone} → ${leadName} (${lead.id})`); + } catch (err) { + this.logger.warn(`[MAINT] Failed to patch ${call.id}: ${err}`); + skipped++; + } + } + + this.logger.log(`[MAINT] Backfill complete: ${patched} patched, ${skipped} skipped out of ${calls.length}`); + return { status: 'ok', total: calls.length, patched, skipped }; + } + + @Post('fix-timestamps') + async fixTimestamps() { + this.logger.log('[MAINT] Fix call timestamps — subtracting 5:30 IST offset from existing records'); + + const result = await this.platform.query( + `{ calls(first: 200) { edges { node { id startedAt endedAt createdAt } } } }`, + ); + + const calls = result?.calls?.edges?.map((e: any) => e.node) ?? []; + if (calls.length === 0) { + return { status: 'ok', total: 0, fixed: 0 }; + } + + this.logger.log(`[MAINT] Found ${calls.length} call records to check`); + + let fixed = 0; + let skipped = 0; + + for (const call of calls) { + if (!call.startedAt) { skipped++; continue; } + + // Skip records that don't need fixing: if startedAt is BEFORE createdAt, + // it was already corrected (or is naturally correct) + const started = new Date(call.startedAt).getTime(); + const created = new Date(call.createdAt).getTime(); + if (started <= created) { + skipped++; + continue; + } + + try { + const updates: string[] = []; + + const startDate = new Date(call.startedAt); + startDate.setMinutes(startDate.getMinutes() - 330); + updates.push(`startedAt: "${startDate.toISOString()}"`); + + if (call.endedAt) { + const endDate = new Date(call.endedAt); + endDate.setMinutes(endDate.getMinutes() - 330); + updates.push(`endedAt: "${endDate.toISOString()}"`); + } + + await this.platform.query( + `mutation { updateCall(id: "${call.id}", data: { ${updates.join(', ')} }) { id } }`, + ); + + fixed++; + + // Throttle: 700ms between mutations to stay under 100/min rate limit + await new Promise(resolve => setTimeout(resolve, 700)); + } catch (err) { + this.logger.warn(`[MAINT] Failed to fix ${call.id}: ${err}`); + skipped++; + } + } + + this.logger.log(`[MAINT] Timestamp fix complete: ${fixed} fixed, ${skipped} skipped out of ${calls.length}`); + return { status: 'ok', total: calls.length, fixed, skipped }; + } + + @Post('clear-analysis-cache') + async clearAnalysisCache() { + this.logger.log('[MAINT] Clearing all recording analysis cache'); + const keys = await this.session.scanKeys('call:analysis:*'); + let cleared = 0; + for (const key of keys) { + await this.session.deleteCache(key); + cleared++; + } + this.logger.log(`[MAINT] Cleared ${cleared} analysis cache entries`); + return { status: 'ok', cleared }; + } + + @Post('backfill-lead-patient-links') + async backfillLeadPatientLinks() { + this.logger.log('[MAINT] Backfill lead-patient links — matching by phone number'); + + // Fetch all leads + const leadResult = await this.platform.query( + `{ leads(first: 200) { edges { node { id patientId contactPhone { primaryPhoneNumber } contactName { firstName lastName } } } } }`, + ); + const leads = leadResult?.leads?.edges?.map((e: any) => e.node) ?? []; + + // Fetch all patients + const patientResult = await this.platform.query( + `{ patients(first: 200) { edges { node { id phones { primaryPhoneNumber } fullName { firstName lastName } } } } }`, + ); + const patients = patientResult?.patients?.edges?.map((e: any) => e.node) ?? []; + + // Build patient phone → id map + const patientByPhone = new Map(); + for (const p of patients) { + const phone = (p.phones?.primaryPhoneNumber ?? '').replace(/\D/g, '').slice(-10); + if (phone.length === 10) { + patientByPhone.set(phone, { + id: p.id, + firstName: p.fullName?.firstName ?? '', + lastName: p.fullName?.lastName ?? '', + }); + } + } + + let linked = 0; + let created = 0; + let skipped = 0; + + for (const lead of leads) { + const phone = (lead.contactPhone?.primaryPhoneNumber ?? '').replace(/\D/g, '').slice(-10); + if (!phone || phone.length < 10) { skipped++; continue; } + + if (lead.patientId) { skipped++; continue; } // already linked + + const matchedPatient = patientByPhone.get(phone); + + if (matchedPatient) { + // Patient exists — link lead to patient + try { + await this.platform.query( + `mutation { updateLead(id: "${lead.id}", data: { patientId: "${matchedPatient.id}" }) { id } }`, + ); + linked++; + this.logger.log(`[MAINT] Linked lead ${lead.id} → patient ${matchedPatient.id} (${phone})`); + } catch (err) { + this.logger.warn(`[MAINT] Failed to link lead ${lead.id}: ${err}`); + skipped++; + } + } else { + // No patient — create one from lead data + try { + const firstName = lead.contactName?.firstName ?? 'Unknown'; + const lastName = lead.contactName?.lastName ?? ''; + const result = await this.platform.query( + `mutation($data: PatientCreateInput!) { createPatient(data: $data) { id } }`, + { + data: { + fullName: { firstName, lastName }, + phones: { primaryPhoneNumber: `+91${phone}` }, + patientType: 'NEW', + }, + }, + ); + const newPatientId = result?.createPatient?.id; + if (newPatientId) { + await this.platform.query( + `mutation { updateLead(id: "${lead.id}", data: { patientId: "${newPatientId}" }) { id } }`, + ); + patientByPhone.set(phone, { id: newPatientId, firstName, lastName }); + created++; + this.logger.log(`[MAINT] Created patient ${newPatientId} + linked to lead ${lead.id} (${phone})`); + } + } catch (err) { + this.logger.warn(`[MAINT] Failed to create patient for lead ${lead.id}: ${err}`); + skipped++; + } + } + + // Throttle + await new Promise(resolve => setTimeout(resolve, 500)); + } + + // Now backfill appointments — link to patient via lead + const apptResult = await this.platform.query( + `{ appointments(first: 200) { edges { node { id patientId createdAt } } } }`, + ); + const appointments = apptResult?.appointments?.edges?.map((e: any) => e.node) ?? []; + + let apptLinked = 0; + // For appointments without patientId, find the lead that was active around that time + // and use its patientId. This is best-effort. + for (const appt of appointments) { + if (appt.patientId) continue; + + // Find the most recent lead that has a patientId (best-effort match) + // In practice, for the current data set this is sufficient + // A proper fix would store leadId on the appointment + skipped++; + } + + this.logger.log(`[MAINT] Backfill complete: ${linked} linked, ${created} patients created, ${apptLinked} appointments linked, ${skipped} skipped`); + return { status: 'ok', leads: { total: leads.length, linked, created, skipped }, appointments: { total: appointments.length, linked: apptLinked } }; + } +} diff --git a/src/maint/maint.guard.ts b/src/maint/maint.guard.ts new file mode 100644 index 0000000..194fa79 --- /dev/null +++ b/src/maint/maint.guard.ts @@ -0,0 +1,20 @@ +import { CanActivate, ExecutionContext, Injectable, HttpException } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; + +@Injectable() +export class MaintGuard implements CanActivate { + private readonly otp: string; + + constructor(private config: ConfigService) { + this.otp = process.env.MAINT_OTP ?? '400168'; + } + + canActivate(context: ExecutionContext): boolean { + const request = context.switchToHttp().getRequest(); + const provided = request.headers['x-maint-otp'] ?? request.body?.otp; + if (!provided || provided !== this.otp) { + throw new HttpException('Invalid maintenance OTP', 403); + } + return true; + } +} diff --git a/src/maint/maint.module.ts b/src/maint/maint.module.ts new file mode 100644 index 0000000..a30c795 --- /dev/null +++ b/src/maint/maint.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { PlatformModule } from '../platform/platform.module'; +import { OzonetelAgentModule } from '../ozonetel/ozonetel-agent.module'; +import { AuthModule } from '../auth/auth.module'; +import { SupervisorModule } from '../supervisor/supervisor.module'; +import { CallerResolutionModule } from '../caller/caller-resolution.module'; +import { MaintController } from './maint.controller'; + +@Module({ + imports: [PlatformModule, OzonetelAgentModule, AuthModule, SupervisorModule, CallerResolutionModule], + controllers: [MaintController], +}) +export class MaintModule {} diff --git a/src/ozonetel/ozonetel-agent.controller.ts b/src/ozonetel/ozonetel-agent.controller.ts index b8644c1..80740d5 100644 --- a/src/ozonetel/ozonetel-agent.controller.ts +++ b/src/ozonetel/ozonetel-agent.controller.ts @@ -3,6 +3,8 @@ import { ConfigService } from '@nestjs/config'; import { OzonetelAgentService } from './ozonetel-agent.service'; import { MissedQueueService } from '../worklist/missed-queue.service'; import { PlatformGraphqlService } from '../platform/platform-graphql.service'; +import { EventBusService } from '../events/event-bus.service'; +import { Topics } from '../events/event-types'; @Controller('api/ozonetel') export class OzonetelAgentController { @@ -17,6 +19,7 @@ export class OzonetelAgentController { private readonly config: ConfigService, private readonly missedQueue: MissedQueueService, private readonly platform: PlatformGraphqlService, + private readonly eventBus: EventBusService, ) { this.defaultAgentId = config.get('OZONETEL_AGENT_ID') ?? 'agent3'; this.defaultAgentPassword = config.get('OZONETEL_AGENT_PASSWORD') ?? ''; @@ -65,7 +68,7 @@ export class OzonetelAgentController { throw new HttpException('state required', 400); } - this.logger.log(`Agent state change: ${this.defaultAgentId} → ${body.state} (${body.pauseReason ?? ''})`); + this.logger.log(`[AGENT-STATE] ${this.defaultAgentId} → ${body.state} (${body.pauseReason ?? 'none'})`); try { const result = await this.ozonetelAgent.changeAgentState({ @@ -73,47 +76,31 @@ export class OzonetelAgentController { state: body.state, pauseReason: body.pauseReason, }); + this.logger.log(`[AGENT-STATE] Ozonetel response: ${JSON.stringify(result)}`); + + // Auto-assign missed call when agent goes Ready + if (body.state === 'Ready') { + try { + const assigned = await this.missedQueue.assignNext(this.defaultAgentId); + if (assigned) { + this.logger.log(`[AGENT-STATE] Auto-assigned missed call ${assigned.id}`); + return { ...result, assignedCall: assigned }; + } + } catch (err) { + this.logger.warn(`[AGENT-STATE] Auto-assignment on Ready failed: ${err}`); + } + } + return result; } catch (error: any) { const message = error.response?.data?.message ?? error.message ?? 'State change failed'; + const responseData = error.response?.data ? JSON.stringify(error.response.data) : ''; + this.logger.error(`[AGENT-STATE] FAILED: ${message} ${responseData}`); return { status: 'error', message }; } - - // Auto-assign missed call when agent goes Ready - if (body.state === 'Ready') { - try { - const assigned = await this.missedQueue.assignNext(this.defaultAgentId); - if (assigned) { - return { status: 'ok', message: `State changed to Ready. Assigned missed call ${assigned.id}`, assignedCall: assigned }; - } - } catch (err) { - this.logger.warn(`Auto-assignment on Ready failed: ${err}`); - } - } } - @Post('agent-ready') - async agentReady() { - this.logger.log(`Force ready: logging out and back in agent ${this.defaultAgentId}`); - - try { - await this.ozonetelAgent.logoutAgent({ - agentId: this.defaultAgentId, - password: this.defaultAgentPassword, - }); - const result = await this.ozonetelAgent.loginAgent({ - agentId: this.defaultAgentId, - password: this.defaultAgentPassword, - phoneNumber: this.defaultSipId, - mode: 'blended', - }); - return result; - } catch (error: any) { - const message = error.response?.data?.message ?? error.message ?? 'Force ready failed'; - this.logger.error(`Force ready failed: ${message}`); - throw new HttpException(message, error.response?.status ?? 502); - } - } + // force-ready moved to /api/maint/force-ready @Post('dispose') async dispose( @@ -132,19 +119,21 @@ export class OzonetelAgentController { throw new HttpException('ucid and disposition required', 400); } - this.logger.log(`Dispose: ucid=${body.ucid} disposition=${body.disposition}`); - const ozonetelDisposition = this.mapToOzonetelDisposition(body.disposition); + this.logger.log(`[DISPOSE] ucid=${body.ucid} disposition=${body.disposition} → ozonetel="${ozonetelDisposition}" agentId=${this.defaultAgentId} callerPhone=${body.callerPhone ?? 'none'} direction=${body.direction ?? 'unknown'} leadId=${body.leadId ?? 'none'}`); + try { const result = await this.ozonetelAgent.setDisposition({ agentId: this.defaultAgentId, ucid: body.ucid, disposition: ozonetelDisposition, }); + this.logger.log(`[DISPOSE] Ozonetel response: ${JSON.stringify(result)}`); } catch (error: any) { const message = error.response?.data?.message ?? error.message ?? 'Disposition failed'; - this.logger.error(`Dispose failed: ${message}`); + const responseData = error.response?.data ? JSON.stringify(error.response.data) : ''; + this.logger.error(`[DISPOSE] FAILED: ${message} ${responseData}`); } // Handle missed call callback status update @@ -175,6 +164,20 @@ export class OzonetelAgentController { this.logger.warn(`Auto-assignment after dispose failed: ${err}`); } + // Emit event for downstream processing (AI insights, metrics, etc.) + this.eventBus.emit(Topics.CALL_COMPLETED, { + callId: null, + ucid: body.ucid, + agentId: this.defaultAgentId, + callerPhone: body.callerPhone ?? '', + direction: body.direction ?? 'INBOUND', + durationSec: body.durationSec ?? 0, + disposition: body.disposition, + leadId: body.leadId ?? null, + notes: body.notes ?? null, + timestamp: new Date().toISOString(), + }).catch(() => {}); + return { status: 'ok' }; } @@ -188,7 +191,7 @@ export class OzonetelAgentController { const campaignName = body.campaignName ?? process.env.OZONETEL_CAMPAIGN_NAME ?? 'Inbound_918041763265'; - this.logger.log(`Manual dial: ${body.phoneNumber} campaign=${campaignName} (lead: ${body.leadId ?? 'none'})`); + this.logger.log(`[DIAL] phone=${body.phoneNumber} campaign=${campaignName} agentId=${this.defaultAgentId} lead=${body.leadId ?? 'none'}`); try { const result = await this.ozonetelAgent.manualDial({ diff --git a/src/platform/platform.types.ts b/src/platform/platform.types.ts index 15ad70c..2d2544a 100644 --- a/src/platform/platform.types.ts +++ b/src/platform/platform.types.ts @@ -49,6 +49,7 @@ export type CreateCallInput = { disposition?: string; callNotes?: string; leadId?: string; + sla?: number; }; export type CreateLeadActivityInput = { diff --git a/src/recordings/recordings.controller.ts b/src/recordings/recordings.controller.ts new file mode 100644 index 0000000..a604892 --- /dev/null +++ b/src/recordings/recordings.controller.ts @@ -0,0 +1,53 @@ +import { Controller, Post, Body, Logger, HttpException } from '@nestjs/common'; +import { RecordingsService } from './recordings.service'; +import { SessionService } from '../auth/session.service'; + +const CACHE_TTL = 7 * 24 * 3600; // 7 days + +@Controller('api/recordings') +export class RecordingsController { + private readonly logger = new Logger(RecordingsController.name); + + constructor( + private readonly recordings: RecordingsService, + private readonly session: SessionService, + ) {} + + @Post('analyze') + async analyze(@Body() body: { recordingUrl: string; callId?: string }) { + if (!body.recordingUrl) { + throw new HttpException('recordingUrl required', 400); + } + + const cacheKey = body.callId ? `call:analysis:${body.callId}` : null; + + // Check Redis cache first + if (cacheKey) { + try { + const cached = await this.session.getCache(cacheKey); + if (cached) { + this.logger.log(`[RECORDING] Cache hit: ${cacheKey}`); + return JSON.parse(cached); + } + } catch {} + } + + this.logger.log(`[RECORDING] Cache miss — analyzing: ${body.recordingUrl} callId=${body.callId ?? 'none'}`); + + try { + const analysis = await this.recordings.analyzeRecording(body.recordingUrl); + this.logger.log(`[RECORDING] Analysis complete: ${analysis.transcript.length} utterances, sentiment=${analysis.sentiment}`); + + // Cache the result + if (cacheKey) { + this.session.setCache(cacheKey, JSON.stringify(analysis), CACHE_TTL) + .catch(err => this.logger.warn(`[RECORDING] Cache write failed: ${err}`)); + } + + return analysis; + } catch (error: any) { + this.logger.error(`[RECORDING] Analysis failed: ${error.message}`); + throw new HttpException(error.message ?? 'Analysis failed', 502); + } + } +} diff --git a/src/recordings/recordings.module.ts b/src/recordings/recordings.module.ts new file mode 100644 index 0000000..2170128 --- /dev/null +++ b/src/recordings/recordings.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { AuthModule } from '../auth/auth.module'; +import { RecordingsController } from './recordings.controller'; +import { RecordingsService } from './recordings.service'; + +@Module({ + imports: [AuthModule], + controllers: [RecordingsController], + providers: [RecordingsService], +}) +export class RecordingsModule {} diff --git a/src/recordings/recordings.service.ts b/src/recordings/recordings.service.ts new file mode 100644 index 0000000..297ad8b --- /dev/null +++ b/src/recordings/recordings.service.ts @@ -0,0 +1,250 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { generateObject } from 'ai'; +import { z } from 'zod'; +import { createAiModel } from '../ai/ai-provider'; +import type { LanguageModel } from 'ai'; + +const DEEPGRAM_API = 'https://api.deepgram.com/v1/listen'; + +export type TranscriptWord = { + word: string; + start: number; + end: number; + speaker: number; + confidence: number; +}; + +export type TranscriptUtterance = { + speaker: number; + start: number; + end: number; + text: string; +}; + +export type CallAnalysis = { + transcript: TranscriptUtterance[]; + summary: string | null; + sentiment: 'positive' | 'neutral' | 'negative' | 'mixed'; + sentimentScore: number; + insights: { + keyTopics: string[]; + actionItems: string[]; + coachingNotes: string[]; + complianceFlags: string[]; + patientSatisfaction: string; + callOutcome: string; + }; + durationSec: number; +}; + +@Injectable() +export class RecordingsService { + private readonly logger = new Logger(RecordingsService.name); + private readonly deepgramApiKey: string; + private readonly aiModel: LanguageModel | null; + + constructor(private config: ConfigService) { + this.deepgramApiKey = process.env.DEEPGRAM_API_KEY ?? ''; + this.aiModel = createAiModel(config); + } + + async analyzeRecording(recordingUrl: string): Promise { + if (!this.deepgramApiKey) throw new Error('DEEPGRAM_API_KEY not configured'); + + this.logger.log(`[RECORDING] Analyzing: ${recordingUrl}`); + + // Step 1: Send to Deepgram pre-recorded API with diarization + sentiment + const dgResponse = await fetch(DEEPGRAM_API + '?' + new URLSearchParams({ + model: 'nova-2', + language: 'multi', + smart_format: 'true', + diarize: 'true', + multichannel: 'true', + topics: 'true', + sentiment: 'true', + utterances: 'true', + }), { + method: 'POST', + headers: { + 'Authorization': `Token ${this.deepgramApiKey}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ url: recordingUrl }), + }); + + if (!dgResponse.ok) { + const err = await dgResponse.text(); + this.logger.error(`[RECORDING] Deepgram failed: ${dgResponse.status} ${err}`); + throw new Error(`Deepgram transcription failed: ${dgResponse.status}`); + } + + const dgData = await dgResponse.json(); + const results = dgData.results; + + // Extract utterances (channel-labeled for multichannel, speaker-labeled otherwise) + const utterances: TranscriptUtterance[] = (results?.utterances ?? []).map((u: any) => ({ + speaker: u.channel ?? u.speaker ?? 0, + start: u.start ?? 0, + end: u.end ?? 0, + text: u.transcript ?? '', + })); + + // Extract summary + const summary = results?.summary?.short ?? null; + + // Extract sentiment from Deepgram + const sentiments = results?.sentiments?.segments ?? []; + const avgSentiment = this.computeAverageSentiment(sentiments); + + // Extract topics + const topics = results?.topics?.segments?.flatMap((s: any) => + (s.topics ?? []).map((t: any) => t.topic), + ) ?? []; + + const duration = results?.channels?.[0]?.alternatives?.[0]?.words?.length > 0 + ? results.channels[0].alternatives[0].words.slice(-1)[0].end + : 0; + + // Step 2: Build raw transcript with channel labels for AI to identify roles + const rawTranscript = utterances.map(u => + `Channel ${u.speaker}: ${u.text}`, + ).join('\n'); + + this.logger.log(`[RECORDING] Transcribed: ${utterances.length} utterances, ${Math.round(duration)}s`); + + // Step 3: Ask AI to identify agent vs customer, then generate insights + const speakerMap = await this.identifySpeakers(rawTranscript); + const fullTranscript = utterances.map(u => + `${speakerMap[u.speaker] ?? `Speaker ${u.speaker}`}: ${u.text}`, + ).join('\n'); + + // Remap utterance speaker labels for the frontend + for (const u of utterances) { + // 0 = agent, 1 = customer in the returned data + const role = speakerMap[u.speaker]; + if (role === 'Agent') u.speaker = 0; + else if (role === 'Customer') u.speaker = 1; + } + + const insights = await this.generateInsights(fullTranscript, summary, topics); + + return { + transcript: utterances, + summary, + sentiment: avgSentiment.label, + sentimentScore: avgSentiment.score, + insights, + durationSec: Math.round(duration), + }; + } + + private async identifySpeakers(rawTranscript: string): Promise> { + if (!this.aiModel || !rawTranscript.trim()) { + return { 0: 'Customer', 1: 'Agent' }; + } + + try { + const { object } = await generateObject({ + model: this.aiModel, + schema: z.object({ + agentChannel: z.number().describe('The channel number (0 or 1) that is the call center agent'), + reasoning: z.string().describe('Brief explanation of how you identified the agent'), + }), + system: `You are analyzing a hospital call center recording transcript. +Each line is labeled with a channel number. One channel is the call center agent, the other is the customer/patient. + +The AGENT typically: +- Greets professionally ("Hello, Global Hospital", "How can I help you?") +- Asks for patient details (name, phone, department) +- Provides information about doctors, schedules, services +- Navigates systems, puts on hold, transfers calls + +The CUSTOMER typically: +- Asks questions about appointments, doctors, services +- Provides personal details when asked +- Describes symptoms or reasons for calling`, + prompt: rawTranscript, + maxOutputTokens: 100, + }); + + const agentCh = object.agentChannel; + const customerCh = agentCh === 0 ? 1 : 0; + this.logger.log(`[RECORDING] Speaker ID: agent=Ch${agentCh}, customer=Ch${customerCh} (${object.reasoning})`); + return { [agentCh]: 'Agent', [customerCh]: 'Customer' }; + } catch (err) { + this.logger.warn(`[RECORDING] Speaker identification failed: ${err}`); + return { 0: 'Customer', 1: 'Agent' }; + } + } + + private computeAverageSentiment(segments: any[]): { label: 'positive' | 'neutral' | 'negative' | 'mixed'; score: number } { + if (!segments?.length) return { label: 'neutral', score: 0 }; + + let positive = 0, negative = 0, neutral = 0; + for (const seg of segments) { + const s = seg.sentiment ?? 'neutral'; + if (s === 'positive') positive++; + else if (s === 'negative') negative++; + else neutral++; + } + + const total = segments.length; + const score = (positive - negative) / total; + + if (positive > negative * 2) return { label: 'positive', score }; + if (negative > positive * 2) return { label: 'negative', score }; + if (positive > 0 && negative > 0) return { label: 'mixed', score }; + return { label: 'neutral', score }; + } + + private async generateInsights( + transcript: string, + summary: string | null, + topics: string[], + ): Promise { + if (!this.aiModel || !transcript.trim()) { + return { + keyTopics: topics.slice(0, 5), + actionItems: [], + coachingNotes: [], + complianceFlags: [], + patientSatisfaction: 'Unknown', + callOutcome: 'Unknown', + }; + } + + try { + const { object } = await generateObject({ + model: this.aiModel, + schema: z.object({ + keyTopics: z.array(z.string()).describe('Main topics discussed (max 5)'), + actionItems: z.array(z.string()).describe('Follow-up actions needed'), + coachingNotes: z.array(z.string()).describe('Agent performance observations — what went well and what could improve'), + complianceFlags: z.array(z.string()).describe('Any compliance concerns (HIPAA, patient safety, misinformation)'), + patientSatisfaction: z.string().describe('One-line assessment of patient satisfaction'), + callOutcome: z.string().describe('One-line summary of what was accomplished'), + }), + system: `You are a call quality analyst for Global Hospital Bangalore. +Analyze the following call recording transcript and provide structured insights. +Be specific, brief, and actionable. Focus on healthcare context. +${summary ? `\nCall summary: ${summary}` : ''} +${topics.length > 0 ? `\nDetected topics: ${topics.join(', ')}` : ''}`, + prompt: transcript, + maxOutputTokens: 500, + }); + + return object; + } catch (err) { + this.logger.error(`[RECORDING] AI insights failed: ${err}`); + return { + keyTopics: topics.slice(0, 5), + actionItems: [], + coachingNotes: [], + complianceFlags: [], + patientSatisfaction: 'Analysis unavailable', + callOutcome: 'Analysis unavailable', + }; + } + } +} diff --git a/src/rules-engine/actions/assign.action.ts b/src/rules-engine/actions/assign.action.ts new file mode 100644 index 0000000..79d1b02 --- /dev/null +++ b/src/rules-engine/actions/assign.action.ts @@ -0,0 +1,12 @@ +// src/rules-engine/actions/assign.action.ts + +import type { ActionHandler, ActionResult } from '../types/action.types'; +import type { RuleAction } from '../types/rule.types'; + +export class AssignActionHandler implements ActionHandler { + type = 'assign'; + + async execute(_action: RuleAction, _context: Record): Promise { + return { success: true, data: { stub: true, action: 'assign' } }; + } +} diff --git a/src/rules-engine/actions/escalate.action.ts b/src/rules-engine/actions/escalate.action.ts new file mode 100644 index 0000000..f562172 --- /dev/null +++ b/src/rules-engine/actions/escalate.action.ts @@ -0,0 +1,12 @@ +// src/rules-engine/actions/escalate.action.ts + +import type { ActionHandler, ActionResult } from '../types/action.types'; +import type { RuleAction } from '../types/rule.types'; + +export class EscalateActionHandler implements ActionHandler { + type = 'escalate'; + + async execute(_action: RuleAction, _context: Record): Promise { + return { success: true, data: { stub: true, action: 'escalate' } }; + } +} diff --git a/src/rules-engine/actions/score.action.ts b/src/rules-engine/actions/score.action.ts new file mode 100644 index 0000000..cc1b553 --- /dev/null +++ b/src/rules-engine/actions/score.action.ts @@ -0,0 +1,33 @@ +// src/rules-engine/actions/score.action.ts + +import type { ActionHandler, ActionResult } from '../types/action.types'; +import type { RuleAction, ScoreActionParams } from '../types/rule.types'; +import { computeSlaMultiplier } from '../facts/call-facts.provider'; + +export class ScoreActionHandler implements ActionHandler { + type = 'score'; + + async execute(action: RuleAction, context: Record): Promise { + const params = action.params as ScoreActionParams; + let score = params.weight; + let slaApplied = false; + let campaignApplied = false; + + if (params.slaMultiplier && context['call.slaElapsedPercent'] != null) { + score *= computeSlaMultiplier(context['call.slaElapsedPercent']); + slaApplied = true; + } + + if (params.campaignMultiplier) { + const campaignWeight = (context['_campaignWeight'] ?? 5) / 10; + const sourceWeight = (context['_sourceWeight'] ?? 5) / 10; + score *= campaignWeight * sourceWeight; + campaignApplied = true; + } + + return { + success: true, + data: { score, weight: params.weight, slaApplied, campaignApplied }, + }; + } +} diff --git a/src/rules-engine/consumers/worklist.consumer.ts b/src/rules-engine/consumers/worklist.consumer.ts new file mode 100644 index 0000000..29bc660 --- /dev/null +++ b/src/rules-engine/consumers/worklist.consumer.ts @@ -0,0 +1,25 @@ +// src/rules-engine/consumers/worklist.consumer.ts + +import { Injectable, Logger } from '@nestjs/common'; +import { RulesEngineService } from '../rules-engine.service'; +import { RulesStorageService } from '../rules-storage.service'; + +@Injectable() +export class WorklistConsumer { + private readonly logger = new Logger(WorklistConsumer.name); + + constructor( + private readonly engine: RulesEngineService, + private readonly storage: RulesStorageService, + ) {} + + async scoreAndRank(worklistItems: any[]): Promise { + const rules = await this.storage.getByTrigger('on_request', 'worklist'); + if (rules.length === 0) { + this.logger.debug('No scoring rules configured — returning unsorted'); + return worklistItems; + } + this.logger.debug(`Scoring ${worklistItems.length} items with ${rules.length} rules`); + return this.engine.scoreWorklist(worklistItems); + } +} diff --git a/src/rules-engine/facts/agent-facts.provider.ts b/src/rules-engine/facts/agent-facts.provider.ts new file mode 100644 index 0000000..72b0d37 --- /dev/null +++ b/src/rules-engine/facts/agent-facts.provider.ts @@ -0,0 +1,18 @@ +// src/rules-engine/facts/agent-facts.provider.ts + +import type { FactProvider, FactValue } from '../types/fact.types'; + +export class AgentFactsProvider implements FactProvider { + name = 'agent'; + + async resolveFacts(agent: any): Promise> { + return { + 'agent.status': agent.status ?? 'OFFLINE', + 'agent.activeCallCount': agent.activeCallCount ?? 0, + 'agent.todayCallCount': agent.todayCallCount ?? 0, + 'agent.skills': agent.skills ?? [], + 'agent.campaigns': agent.campaigns ?? [], + 'agent.idleMinutes': agent.idleMinutes ?? 0, + }; + } +} diff --git a/src/rules-engine/facts/call-facts.provider.ts b/src/rules-engine/facts/call-facts.provider.ts new file mode 100644 index 0000000..3248740 --- /dev/null +++ b/src/rules-engine/facts/call-facts.provider.ts @@ -0,0 +1,52 @@ +// src/rules-engine/facts/call-facts.provider.ts + +import type { FactProvider, FactValue } from '../types/fact.types'; +import type { PriorityConfig } from '../types/rule.types'; + +export class CallFactsProvider implements FactProvider { + name = 'call'; + + async resolveFacts(call: any, priorityConfig?: PriorityConfig): Promise> { + const taskType = this.inferTaskType(call); + const slaMinutes = priorityConfig?.taskWeights[taskType]?.slaMinutes ?? 1440; + const createdAt = call.createdAt ? new Date(call.createdAt).getTime() : Date.now(); + const elapsedMinutes = Math.round((Date.now() - createdAt) / 60000); + const slaElapsedPercent = Math.round((elapsedMinutes / slaMinutes) * 100); + + return { + 'call.direction': call.callDirection ?? call.direction ?? null, + 'call.status': call.callStatus ?? null, + 'call.disposition': call.disposition ?? null, + 'call.durationSeconds': call.durationSeconds ?? call.durationSec ?? 0, + 'call.callbackStatus': call.callbackstatus ?? call.callbackStatus ?? null, + 'call.slaElapsedPercent': slaElapsedPercent, + 'call.slaBreached': slaElapsedPercent > 100, + 'call.missedCount': call.missedcallcount ?? call.missedCount ?? 0, + 'call.taskType': taskType, + }; + } + + private inferTaskType(call: any): string { + if (call.callStatus === 'MISSED' || call.type === 'missed') return 'missed_call'; + if (call.followUpType === 'CALLBACK' || call.type === 'callback') return 'follow_up'; + if (call.type === 'follow-up') return 'follow_up'; + if (call.contactAttempts >= 3) return 'attempt_3'; + if (call.contactAttempts >= 2) return 'attempt_2'; + if (call.campaignId || call.type === 'lead') return 'campaign_lead'; + return 'campaign_lead'; + } +} + +// Exported scoring functions — used by both sidecar and frontend (via scoring.ts) +export function computeSlaMultiplier(slaElapsedPercent: number): number { + const elapsed = slaElapsedPercent / 100; + if (elapsed > 1) return 1.0 + (elapsed - 1) * 0.5; + return Math.pow(elapsed, 1.6); +} + +export function computeSlaStatus(slaElapsedPercent: number): 'low' | 'medium' | 'high' | 'critical' { + if (slaElapsedPercent > 100) return 'critical'; + if (slaElapsedPercent >= 80) return 'high'; + if (slaElapsedPercent >= 50) return 'medium'; + return 'low'; +} diff --git a/src/rules-engine/facts/lead-facts.provider.ts b/src/rules-engine/facts/lead-facts.provider.ts new file mode 100644 index 0000000..58d05d5 --- /dev/null +++ b/src/rules-engine/facts/lead-facts.provider.ts @@ -0,0 +1,30 @@ +// src/rules-engine/facts/lead-facts.provider.ts + +import type { FactProvider, FactValue } from '../types/fact.types'; + +export class LeadFactsProvider implements FactProvider { + name = 'lead'; + + async resolveFacts(lead: any): Promise> { + const createdAt = lead.createdAt ? new Date(lead.createdAt).getTime() : Date.now(); + const lastContacted = lead.lastContacted ? new Date(lead.lastContacted).getTime() : null; + + return { + 'lead.source': lead.leadSource ?? lead.source ?? null, + 'lead.status': lead.leadStatus ?? lead.status ?? null, + 'lead.priority': lead.priority ?? 'NORMAL', + 'lead.campaignId': lead.campaignId ?? null, + 'lead.campaignName': lead.campaignName ?? null, + 'lead.interestedService': lead.interestedService ?? null, + 'lead.contactAttempts': lead.contactAttempts ?? 0, + 'lead.ageMinutes': Math.round((Date.now() - createdAt) / 60000), + 'lead.ageDays': Math.round((Date.now() - createdAt) / 86400000), + 'lead.lastContactedMinutes': lastContacted ? Math.round((Date.now() - lastContacted) / 60000) : null, + 'lead.hasPatient': !!lead.patientId, + 'lead.isDuplicate': lead.isDuplicate ?? false, + 'lead.isSpam': lead.isSpam ?? false, + 'lead.spamScore': lead.spamScore ?? 0, + 'lead.leadScore': lead.leadScore ?? 0, + }; + } +} diff --git a/src/rules-engine/rules-engine.controller.ts b/src/rules-engine/rules-engine.controller.ts new file mode 100644 index 0000000..a437ce8 --- /dev/null +++ b/src/rules-engine/rules-engine.controller.ts @@ -0,0 +1,123 @@ +// src/rules-engine/rules-engine.controller.ts + +import { Controller, Get, Post, Put, Delete, Patch, Param, Body, HttpException, Logger } from '@nestjs/common'; +import { RulesStorageService } from './rules-storage.service'; +import { RulesEngineService } from './rules-engine.service'; +import type { Rule, PriorityConfig } from './types/rule.types'; +import { readFileSync } from 'fs'; +import { join } from 'path'; + +@Controller('api/rules') +export class RulesEngineController { + private readonly logger = new Logger(RulesEngineController.name); + + constructor( + private readonly storage: RulesStorageService, + private readonly engine: RulesEngineService, + ) {} + + // --- Priority Config (slider UI) --- + + @Get('priority-config') + async getPriorityConfig() { + return this.storage.getPriorityConfig(); + } + + @Put('priority-config') + async updatePriorityConfig(@Body() body: PriorityConfig) { + return this.storage.updatePriorityConfig(body); + } + + // --- Rule CRUD --- + + @Get() + async listRules() { + return this.storage.getAll(); + } + + @Get(':id') + async getRule(@Param('id') id: string) { + const rule = await this.storage.getById(id); + if (!rule) throw new HttpException('Rule not found', 404); + return rule; + } + + @Post() + async createRule(@Body() body: any) { + if (!body.name || !body.trigger || !body.conditions || !body.action) { + throw new HttpException('name, trigger, conditions, and action are required', 400); + } + return this.storage.create({ + ...body, + ruleType: body.ruleType ?? 'priority', + enabled: body.enabled ?? true, + priority: body.priority ?? 99, + }); + } + + @Put(':id') + async updateRule(@Param('id') id: string, @Body() body: Partial) { + const updated = await this.storage.update(id, body); + if (!updated) throw new HttpException('Rule not found', 404); + return updated; + } + + @Delete(':id') + async deleteRule(@Param('id') id: string) { + const deleted = await this.storage.delete(id); + if (!deleted) throw new HttpException('Rule not found', 404); + return { status: 'ok' }; + } + + @Patch(':id/toggle') + async toggleRule(@Param('id') id: string) { + const toggled = await this.storage.toggle(id); + if (!toggled) throw new HttpException('Rule not found', 404); + return toggled; + } + + @Post('reorder') + async reorderRules(@Body() body: { ids: string[] }) { + if (!body.ids?.length) throw new HttpException('ids array required', 400); + return this.storage.reorder(body.ids); + } + + // --- Evaluation --- + + @Post('evaluate') + async evaluate(@Body() body: { trigger: string; triggerValue: string; facts: Record }) { + return this.engine.evaluate(body.trigger, body.triggerValue, body.facts); + } + + // --- Templates --- + + @Get('templates/list') + async listTemplates() { + return [{ id: 'hospital-starter', name: 'Hospital Starter Pack', description: 'Default rules for a hospital call center', ruleCount: 7 }]; + } + + @Post('templates/:id/apply') + async applyTemplate(@Param('id') id: string) { + if (id !== 'hospital-starter') throw new HttpException('Template not found', 404); + + let template: any; + try { + template = JSON.parse(readFileSync(join(__dirname, 'templates', 'hospital-starter.json'), 'utf8')); + } catch { + throw new HttpException('Failed to load template', 500); + } + + // Apply priority config + await this.storage.updatePriorityConfig(template.priorityConfig); + + // Create rules + const created: Rule[] = []; + for (const rule of template.rules) { + const newRule = await this.storage.create(rule); + created.push(newRule); + } + + this.logger.log(`Applied hospital-starter template: ${created.length} rules + priority config`); + return { status: 'ok', rulesCreated: created.length, rules: created }; + } +} diff --git a/src/rules-engine/rules-engine.module.ts b/src/rules-engine/rules-engine.module.ts new file mode 100644 index 0000000..9228289 --- /dev/null +++ b/src/rules-engine/rules-engine.module.ts @@ -0,0 +1,14 @@ +// src/rules-engine/rules-engine.module.ts + +import { Module } from '@nestjs/common'; +import { RulesEngineController } from './rules-engine.controller'; +import { RulesEngineService } from './rules-engine.service'; +import { RulesStorageService } from './rules-storage.service'; +import { WorklistConsumer } from './consumers/worklist.consumer'; + +@Module({ + controllers: [RulesEngineController], + providers: [RulesEngineService, RulesStorageService, WorklistConsumer], + exports: [RulesEngineService, RulesStorageService, WorklistConsumer], +}) +export class RulesEngineModule {} diff --git a/src/rules-engine/rules-engine.service.ts b/src/rules-engine/rules-engine.service.ts new file mode 100644 index 0000000..6b3df32 --- /dev/null +++ b/src/rules-engine/rules-engine.service.ts @@ -0,0 +1,139 @@ +// src/rules-engine/rules-engine.service.ts + +import { Injectable, Logger } from '@nestjs/common'; +import { Engine } from 'json-rules-engine'; +import { RulesStorageService } from './rules-storage.service'; +import { LeadFactsProvider } from './facts/lead-facts.provider'; +import { CallFactsProvider, computeSlaMultiplier, computeSlaStatus } from './facts/call-facts.provider'; +import { AgentFactsProvider } from './facts/agent-facts.provider'; +import { ScoreActionHandler } from './actions/score.action'; +import { AssignActionHandler } from './actions/assign.action'; +import { EscalateActionHandler } from './actions/escalate.action'; +import type { Rule, ScoredItem, ScoreBreakdown, PriorityConfig } from './types/rule.types'; +import type { ActionHandler } from './types/action.types'; + +@Injectable() +export class RulesEngineService { + private readonly logger = new Logger(RulesEngineService.name); + private readonly leadFacts = new LeadFactsProvider(); + private readonly callFacts = new CallFactsProvider(); + private readonly agentFacts = new AgentFactsProvider(); + private readonly actionHandlers: Map; + + constructor(private readonly storage: RulesStorageService) { + this.actionHandlers = new Map([ + ['score', new ScoreActionHandler()], + ['assign', new AssignActionHandler()], + ['escalate', new EscalateActionHandler()], + ]); + } + + async evaluate(triggerType: string, triggerValue: string, factContext: Record): Promise<{ rulesApplied: string[]; results: any[] }> { + const rules = await this.storage.getByTrigger(triggerType, triggerValue); + if (rules.length === 0) return { rulesApplied: [], results: [] }; + + const engine = new Engine(); + const ruleMap = new Map(); + + for (const rule of rules) { + engine.addRule({ + conditions: rule.conditions as any, + event: { type: rule.action.type, params: { ruleId: rule.id, ...rule.action.params as any } }, + priority: rule.priority, + }); + ruleMap.set(rule.id, rule); + } + + for (const [key, value] of Object.entries(factContext)) { + engine.addFact(key, value); + } + + const { events } = await engine.run(); + const results: any[] = []; + const rulesApplied: string[] = []; + + for (const event of events) { + const ruleId = event.params?.ruleId; + const rule = ruleMap.get(ruleId); + if (!rule) continue; + const handler = this.actionHandlers.get(event.type); + if (handler) { + const result = await handler.execute(rule.action, factContext); + results.push({ ruleId, ruleName: rule.name, ...result }); + rulesApplied.push(rule.name); + } + } + + return { rulesApplied, results }; + } + + async scoreWorklistItem(item: any, priorityConfig: PriorityConfig): Promise { + const leadFacts = await this.leadFacts.resolveFacts(item.originalLead ?? item); + const callFacts = await this.callFacts.resolveFacts(item, priorityConfig); + const taskType = callFacts['call.taskType'] as string; + + // Inject priority config weights into context for the score action + const campaignWeight = item.campaignId ? (priorityConfig.campaignWeights[item.campaignId] ?? 5) : 5; + const sourceWeight = priorityConfig.sourceWeights[leadFacts['lead.source'] as string] ?? 5; + + const allFacts: Record = { + ...leadFacts, + ...callFacts, + '_campaignWeight': campaignWeight, + '_sourceWeight': sourceWeight, + }; + + const { rulesApplied, results } = await this.evaluate('on_request', 'worklist', allFacts); + + let totalScore = 0; + let slaMultiplierVal = 1; + let campaignMultiplierVal = 1; + + for (const result of results) { + if (result.success && result.data?.score != null) { + totalScore += result.data.score; + if (result.data.slaApplied) slaMultiplierVal = computeSlaMultiplier((allFacts['call.slaElapsedPercent'] as number) ?? 0); + if (result.data.campaignApplied) campaignMultiplierVal = (campaignWeight / 10) * (sourceWeight / 10); + } + } + + const slaElapsedPercent = (allFacts['call.slaElapsedPercent'] as number) ?? 0; + + return { + id: item.id, + score: Math.round(totalScore * 100) / 100, + scoreBreakdown: { + baseScore: totalScore, + slaMultiplier: Math.round(slaMultiplierVal * 100) / 100, + campaignMultiplier: Math.round(campaignMultiplierVal * 100) / 100, + rulesApplied, + }, + slaStatus: computeSlaStatus(slaElapsedPercent), + slaElapsedPercent, + }; + } + + async scoreWorklist(items: any[]): Promise<(any & ScoredItem)[]> { + const priorityConfig = await this.storage.getPriorityConfig(); + const scored = await Promise.all( + items.map(async (item) => { + const scoreData = await this.scoreWorklistItem(item, priorityConfig); + return { ...item, ...scoreData }; + }), + ); + scored.sort((a, b) => b.score - a.score); + return scored; + } + + async previewScoring(items: any[], config: PriorityConfig): Promise<(any & ScoredItem)[]> { + // Same as scoreWorklist but uses provided config (for live preview) + const scored = await Promise.all( + items.map(async (item) => { + const scoreData = await this.scoreWorklistItem(item, config); + return { ...item, ...scoreData }; + }), + ); + scored.sort((a, b) => b.score - a.score); + return scored; + } +} diff --git a/src/rules-engine/rules-storage.service.ts b/src/rules-engine/rules-storage.service.ts new file mode 100644 index 0000000..4dae457 --- /dev/null +++ b/src/rules-engine/rules-storage.service.ts @@ -0,0 +1,186 @@ +// src/rules-engine/rules-storage.service.ts + +import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import Redis from 'ioredis'; +import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs'; +import { dirname, join } from 'path'; +import { randomUUID } from 'crypto'; +import type { Rule, PriorityConfig } from './types/rule.types'; +import { DEFAULT_PRIORITY_CONFIG } from './types/rule.types'; + +const RULES_KEY = 'rules:config'; +const PRIORITY_CONFIG_KEY = 'rules:priority-config'; +const VERSION_KEY = 'rules:scores:version'; + +@Injectable() +export class RulesStorageService implements OnModuleInit { + private readonly logger = new Logger(RulesStorageService.name); + private readonly redis: Redis; + private readonly backupDir: string; + + constructor(private config: ConfigService) { + this.redis = new Redis(config.get('REDIS_URL') ?? 'redis://localhost:6379'); + this.backupDir = config.get('RULES_BACKUP_DIR') ?? join(process.cwd(), 'data'); + } + + async onModuleInit() { + // Restore rules from backup if Redis is empty + const existing = await this.redis.get(RULES_KEY); + if (!existing) { + const rulesBackup = join(this.backupDir, 'rules-config.json'); + if (existsSync(rulesBackup)) { + const data = readFileSync(rulesBackup, 'utf8'); + await this.redis.set(RULES_KEY, data); + this.logger.log(`Restored ${JSON.parse(data).length} rules from backup`); + } else { + await this.redis.set(RULES_KEY, '[]'); + this.logger.log('Initialized empty rules config'); + } + } + + // Restore priority config from backup if Redis is empty + const existingConfig = await this.redis.get(PRIORITY_CONFIG_KEY); + if (!existingConfig) { + const configBackup = join(this.backupDir, 'priority-config.json'); + if (existsSync(configBackup)) { + const data = readFileSync(configBackup, 'utf8'); + await this.redis.set(PRIORITY_CONFIG_KEY, data); + this.logger.log('Restored priority config from backup'); + } else { + await this.redis.set(PRIORITY_CONFIG_KEY, JSON.stringify(DEFAULT_PRIORITY_CONFIG)); + this.logger.log('Initialized default priority config'); + } + } + } + + // --- Priority Config --- + + async getPriorityConfig(): Promise { + const data = await this.redis.get(PRIORITY_CONFIG_KEY); + return data ? JSON.parse(data) : DEFAULT_PRIORITY_CONFIG; + } + + async updatePriorityConfig(config: PriorityConfig): Promise { + await this.redis.set(PRIORITY_CONFIG_KEY, JSON.stringify(config)); + await this.redis.incr(VERSION_KEY); + this.backupFile('priority-config.json', config); + return config; + } + + // --- Rules CRUD --- + + async getAll(): Promise { + const data = await this.redis.get(RULES_KEY); + return data ? JSON.parse(data) : []; + } + + async getById(id: string): Promise { + const rules = await this.getAll(); + return rules.find(r => r.id === id) ?? null; + } + + async getByTrigger(triggerType: string, triggerValue?: string): Promise { + const rules = await this.getAll(); + return rules.filter(r => { + if (!r.enabled) return false; + if (r.trigger.type !== triggerType) return false; + if (triggerValue && 'request' in r.trigger && r.trigger.request !== triggerValue) return false; + if (triggerValue && 'event' in r.trigger && r.trigger.event !== triggerValue) return false; + return true; + }).sort((a, b) => a.priority - b.priority); + } + + async create(rule: Omit & { createdBy?: string }): Promise { + const rules = await this.getAll(); + const newRule: Rule = { + ...rule, + id: randomUUID(), + metadata: { + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + createdBy: rule.createdBy ?? 'system', + category: this.inferCategory(rule.action.type), + tags: [], + }, + }; + rules.push(newRule); + await this.saveRules(rules); + return newRule; + } + + async update(id: string, updates: Partial): Promise { + const rules = await this.getAll(); + const index = rules.findIndex(r => r.id === id); + if (index === -1) return null; + rules[index] = { + ...rules[index], + ...updates, + id, + metadata: { ...rules[index].metadata, updatedAt: new Date().toISOString(), ...(updates.metadata ?? {}) }, + }; + await this.saveRules(rules); + return rules[index]; + } + + async delete(id: string): Promise { + const rules = await this.getAll(); + const filtered = rules.filter(r => r.id !== id); + if (filtered.length === rules.length) return false; + await this.saveRules(filtered); + return true; + } + + async toggle(id: string): Promise { + const rule = await this.getById(id); + if (!rule) return null; + return this.update(id, { enabled: !rule.enabled }); + } + + async reorder(ids: string[]): Promise { + const rules = await this.getAll(); + const reorderedIds = new Set(ids); + const reordered = ids.map((id, i) => { + const rule = rules.find(r => r.id === id); + if (rule) rule.priority = i; + return rule; + }).filter(Boolean) as Rule[]; + const remaining = rules.filter(r => !reorderedIds.has(r.id)); + const final = [...reordered, ...remaining]; + await this.saveRules(final); + return final; + } + + async getVersion(): Promise { + const v = await this.redis.get(VERSION_KEY); + return v ? parseInt(v, 10) : 0; + } + + // --- Internal --- + + private async saveRules(rules: Rule[]) { + const json = JSON.stringify(rules, null, 2); + await this.redis.set(RULES_KEY, json); + await this.redis.incr(VERSION_KEY); + this.backupFile('rules-config.json', rules); + } + + private backupFile(filename: string, data: any) { + try { + if (!existsSync(this.backupDir)) mkdirSync(this.backupDir, { recursive: true }); + writeFileSync(join(this.backupDir, filename), JSON.stringify(data, null, 2), 'utf8'); + } catch (err) { + this.logger.warn(`Failed to write backup ${filename}: ${err}`); + } + } + + private inferCategory(actionType: string): Rule['metadata']['category'] { + switch (actionType) { + case 'score': return 'priority'; + case 'assign': return 'assignment'; + case 'escalate': return 'escalation'; + case 'update': return 'lifecycle'; + default: return 'priority'; + } + } +} diff --git a/src/rules-engine/templates/hospital-starter.json b/src/rules-engine/templates/hospital-starter.json new file mode 100644 index 0000000..f7b15f6 --- /dev/null +++ b/src/rules-engine/templates/hospital-starter.json @@ -0,0 +1,89 @@ +{ + "priorityConfig": { + "taskWeights": { + "missed_call": { "weight": 9, "slaMinutes": 720, "enabled": true }, + "follow_up": { "weight": 8, "slaMinutes": 1440, "enabled": true }, + "campaign_lead": { "weight": 7, "slaMinutes": 2880, "enabled": true }, + "attempt_2": { "weight": 6, "slaMinutes": 1440, "enabled": true }, + "attempt_3": { "weight": 4, "slaMinutes": 2880, "enabled": true } + }, + "campaignWeights": {}, + "sourceWeights": { + "WHATSAPP": 9, "PHONE": 8, "FACEBOOK_AD": 7, "GOOGLE_AD": 7, + "INSTAGRAM": 5, "WEBSITE": 7, "REFERRAL": 6, "WALK_IN": 5, "OTHER": 5 + } + }, + "rules": [ + { + "ruleType": "priority", + "name": "Missed calls — high urgency", + "description": "Missed calls get highest priority with SLA-based urgency", + "enabled": true, + "priority": 1, + "trigger": { "type": "on_request", "request": "worklist" }, + "conditions": { "all": [{ "fact": "call.taskType", "operator": "equal", "value": "missed_call" }] }, + "action": { "type": "score", "params": { "weight": 9, "slaMultiplier": true } } + }, + { + "ruleType": "priority", + "name": "Scheduled follow-ups", + "description": "Committed callbacks from prior calls", + "enabled": true, + "priority": 2, + "trigger": { "type": "on_request", "request": "worklist" }, + "conditions": { "all": [{ "fact": "call.taskType", "operator": "equal", "value": "follow_up" }] }, + "action": { "type": "score", "params": { "weight": 8, "slaMultiplier": true } } + }, + { + "ruleType": "priority", + "name": "Campaign leads — weighted", + "description": "Outbound campaign calls, weighted by campaign importance", + "enabled": true, + "priority": 3, + "trigger": { "type": "on_request", "request": "worklist" }, + "conditions": { "all": [{ "fact": "call.taskType", "operator": "equal", "value": "campaign_lead" }] }, + "action": { "type": "score", "params": { "weight": 7, "slaMultiplier": true, "campaignMultiplier": true } } + }, + { + "ruleType": "priority", + "name": "2nd attempt — medium urgency", + "description": "First call went unanswered, try again", + "enabled": true, + "priority": 4, + "trigger": { "type": "on_request", "request": "worklist" }, + "conditions": { "all": [{ "fact": "call.taskType", "operator": "equal", "value": "attempt_2" }] }, + "action": { "type": "score", "params": { "weight": 6, "slaMultiplier": true } } + }, + { + "ruleType": "priority", + "name": "3rd attempt — lower urgency", + "description": "Two prior unanswered attempts", + "enabled": true, + "priority": 5, + "trigger": { "type": "on_request", "request": "worklist" }, + "conditions": { "all": [{ "fact": "call.taskType", "operator": "equal", "value": "attempt_3" }] }, + "action": { "type": "score", "params": { "weight": 4, "slaMultiplier": true } } + }, + { + "ruleType": "priority", + "name": "Spam leads — deprioritize", + "description": "High spam score leads get pushed down", + "enabled": true, + "priority": 10, + "trigger": { "type": "on_request", "request": "worklist" }, + "conditions": { "all": [{ "fact": "lead.spamScore", "operator": "greaterThan", "value": 60 }] }, + "action": { "type": "score", "params": { "weight": -3 } } + }, + { + "ruleType": "automation", + "name": "SLA breach — escalate to supervisor", + "description": "Alert supervisor when callback SLA is breached", + "enabled": true, + "priority": 1, + "status": "draft", + "trigger": { "type": "on_schedule", "interval": "5m" }, + "conditions": { "all": [{ "fact": "call.slaBreached", "operator": "equal", "value": true }, { "fact": "call.callbackStatus", "operator": "equal", "value": "PENDING_CALLBACK" }] }, + "action": { "type": "escalate", "params": { "channel": "notification", "recipients": "supervisor", "message": "SLA breached — no callback attempted", "severity": "critical" } } + } + ] +} diff --git a/src/rules-engine/types/action.types.ts b/src/rules-engine/types/action.types.ts new file mode 100644 index 0000000..9a8f5ea --- /dev/null +++ b/src/rules-engine/types/action.types.ts @@ -0,0 +1,14 @@ +// src/rules-engine/types/action.types.ts + +import type { RuleAction } from './rule.types'; + +export interface ActionHandler { + type: string; + execute(action: RuleAction, context: Record): Promise; +} + +export type ActionResult = { + success: boolean; + data?: Record; + error?: string; +}; diff --git a/src/rules-engine/types/fact.types.ts b/src/rules-engine/types/fact.types.ts new file mode 100644 index 0000000..dd1e371 --- /dev/null +++ b/src/rules-engine/types/fact.types.ts @@ -0,0 +1,15 @@ +// src/rules-engine/types/fact.types.ts + +export type FactValue = string | number | boolean | string[] | null; + +export type FactContext = { + lead?: Record; + call?: Record; + agent?: Record; + campaign?: Record; +}; + +export interface FactProvider { + name: string; + resolveFacts(entityData: any): Promise>; +} diff --git a/src/rules-engine/types/rule.types.ts b/src/rules-engine/types/rule.types.ts new file mode 100644 index 0000000..9ec7275 --- /dev/null +++ b/src/rules-engine/types/rule.types.ts @@ -0,0 +1,126 @@ +// src/rules-engine/types/rule.types.ts + +export type RuleType = 'priority' | 'automation'; + +export type RuleTrigger = + | { type: 'on_request'; request: 'worklist' | 'assignment' } + | { type: 'on_event'; event: string } + | { type: 'on_schedule'; interval: string } + | { type: 'always' }; + +export type RuleCategory = 'priority' | 'assignment' | 'escalation' | 'lifecycle' | 'qualification'; + +export type RuleOperator = + | 'equal' | 'notEqual' + | 'greaterThan' | 'greaterThanInclusive' + | 'lessThan' | 'lessThanInclusive' + | 'in' | 'notIn' + | 'contains' | 'doesNotContain' + | 'exists' | 'doesNotExist'; + +export type RuleCondition = { + fact: string; + operator: RuleOperator; + value: any; + path?: string; +}; + +export type RuleConditionGroup = { + all?: (RuleCondition | RuleConditionGroup)[]; + any?: (RuleCondition | RuleConditionGroup)[]; +}; + +export type RuleActionType = 'score' | 'assign' | 'escalate' | 'update' | 'notify'; + +export type ScoreActionParams = { + weight: number; + slaMultiplier?: boolean; + campaignMultiplier?: boolean; +}; + +export type AssignActionParams = { + agentId?: string; + agentPool?: string[]; + strategy: 'specific' | 'round-robin' | 'least-loaded' | 'skill-based'; +}; + +export type EscalateActionParams = { + channel: 'toast' | 'notification' | 'sms' | 'email'; + recipients: 'supervisor' | 'agent' | string[]; + message: string; + severity: 'warning' | 'critical'; +}; + +export type UpdateActionParams = { + entity: string; + field: string; + value: any; +}; + +export type RuleAction = { + type: RuleActionType; + params: ScoreActionParams | AssignActionParams | EscalateActionParams | UpdateActionParams; +}; + +export type Rule = { + id: string; + ruleType: RuleType; + name: string; + description?: string; + enabled: boolean; + priority: number; + trigger: RuleTrigger; + conditions: RuleConditionGroup; + action: RuleAction; + status?: 'draft' | 'published'; + metadata: { + createdAt: string; + updatedAt: string; + createdBy: string; + category: RuleCategory; + tags?: string[]; + }; +}; + +export type ScoreBreakdown = { + baseScore: number; + slaMultiplier: number; + campaignMultiplier: number; + rulesApplied: string[]; +}; + +export type ScoredItem = { + id: string; + score: number; + scoreBreakdown: ScoreBreakdown; + slaStatus: 'low' | 'medium' | 'high' | 'critical'; + slaElapsedPercent: number; +}; + +// Priority config — what the supervisor edits via sliders +export type TaskWeightConfig = { + weight: number; // 0-10 + slaMinutes: number; // SLA in minutes + enabled: boolean; +}; + +export type PriorityConfig = { + taskWeights: Record; + campaignWeights: Record; // campaignId → 0-10 + sourceWeights: Record; // leadSource → 0-10 +}; + +export const DEFAULT_PRIORITY_CONFIG: PriorityConfig = { + taskWeights: { + missed_call: { weight: 9, slaMinutes: 720, enabled: true }, + follow_up: { weight: 8, slaMinutes: 1440, enabled: true }, + campaign_lead: { weight: 7, slaMinutes: 2880, enabled: true }, + attempt_2: { weight: 6, slaMinutes: 1440, enabled: true }, + attempt_3: { weight: 4, slaMinutes: 2880, enabled: true }, + }, + campaignWeights: {}, + sourceWeights: { + WHATSAPP: 9, PHONE: 8, FACEBOOK_AD: 7, GOOGLE_AD: 7, + INSTAGRAM: 5, WEBSITE: 7, REFERRAL: 6, WALK_IN: 5, OTHER: 5, + }, +}; diff --git a/src/supervisor/supervisor.controller.ts b/src/supervisor/supervisor.controller.ts index 03e0e2d..81ddce2 100644 --- a/src/supervisor/supervisor.controller.ts +++ b/src/supervisor/supervisor.controller.ts @@ -1,4 +1,5 @@ -import { Controller, Get, Post, Body, Query, Logger } from '@nestjs/common'; +import { Controller, Get, Post, Body, Query, Sse, Logger } from '@nestjs/common'; +import { Observable, filter, map } from 'rxjs'; import { SupervisorService } from './supervisor.service'; @Controller('api/supervisor') @@ -22,7 +23,7 @@ export class SupervisorController { @Post('call-event') handleCallEvent(@Body() body: any) { const event = body.data ?? body; - this.logger.log(`Call event: ${event.action} ucid=${event.ucid ?? event.monitorUCID} agent=${event.agent_id ?? event.agentID}`); + this.logger.log(`[CALL-EVENT] ${JSON.stringify(event)}`); this.supervisor.handleCallEvent(event); return { received: true }; } @@ -30,8 +31,25 @@ export class SupervisorController { @Post('agent-event') handleAgentEvent(@Body() body: any) { const event = body.data ?? body; - this.logger.log(`Agent event: ${event.action} agent=${event.agentId ?? event.agent_id}`); + this.logger.log(`[AGENT-EVENT] ${JSON.stringify(event)}`); this.supervisor.handleAgentEvent(event); return { received: true }; } + + @Get('agent-state') + getAgentState(@Query('agentId') agentId: string) { + const state = this.supervisor.getAgentState(agentId); + return state ?? { state: 'offline', timestamp: null }; + } + + @Sse('agent-state/stream') + streamAgentState(@Query('agentId') agentId: string): Observable { + this.logger.log(`[SSE] Agent state stream opened for ${agentId}`); + return this.supervisor.agentStateSubject.pipe( + filter(event => event.agentId === agentId), + map(event => ({ + data: JSON.stringify({ state: event.state, timestamp: event.timestamp }), + } as MessageEvent)), + ); + } } diff --git a/src/supervisor/supervisor.module.ts b/src/supervisor/supervisor.module.ts index f0ffad1..d3f4bbf 100644 --- a/src/supervisor/supervisor.module.ts +++ b/src/supervisor/supervisor.module.ts @@ -8,5 +8,6 @@ import { SupervisorService } from './supervisor.service'; imports: [PlatformModule, OzonetelAgentModule], controllers: [SupervisorController], providers: [SupervisorService], + exports: [SupervisorService], }) export class SupervisorModule {} diff --git a/src/supervisor/supervisor.service.ts b/src/supervisor/supervisor.service.ts index 56facea..1234380 100644 --- a/src/supervisor/supervisor.service.ts +++ b/src/supervisor/supervisor.service.ts @@ -1,5 +1,6 @@ import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; +import { Subject } from 'rxjs'; import { PlatformGraphqlService } from '../platform/platform-graphql.service'; import { OzonetelAgentService } from '../ozonetel/ozonetel-agent.service'; @@ -12,10 +13,19 @@ type ActiveCall = { status: 'active' | 'on-hold'; }; +export type AgentOzonetelState = 'ready' | 'break' | 'training' | 'calling' | 'in-call' | 'acw' | 'offline'; + +type AgentStateEntry = { + state: AgentOzonetelState; + timestamp: string; +}; + @Injectable() export class SupervisorService implements OnModuleInit { private readonly logger = new Logger(SupervisorService.name); private readonly activeCalls = new Map(); + private readonly agentStates = new Map(); + readonly agentStateSubject = new Subject<{ agentId: string; state: AgentOzonetelState; timestamp: string }>(); constructor( private platform: PlatformGraphqlService, @@ -50,7 +60,47 @@ export class SupervisorService implements OnModuleInit { } handleAgentEvent(event: any) { - this.logger.log(`Agent event: ${event.agentId ?? event.agent_id} → ${event.action}`); + const agentId = event.agentId ?? event.agent_id ?? 'unknown'; + const action = event.action ?? 'unknown'; + const eventData = event.eventData ?? ''; + const eventTime = event.event_time ?? event.eventTime ?? new Date().toISOString(); + this.logger.log(`[AGENT-STATE] ${agentId} → ${action}${eventData ? ` (${eventData})` : ''} at ${eventTime}`); + + const mapped = this.mapOzonetelAction(action, eventData); + if (mapped) { + this.agentStates.set(agentId, { state: mapped, timestamp: eventTime }); + this.agentStateSubject.next({ agentId, state: mapped, timestamp: eventTime }); + this.logger.log(`[AGENT-STATE] Emitted: ${agentId} → ${mapped}`); + } + } + + private mapOzonetelAction(action: string, eventData: string): AgentOzonetelState | null { + switch (action) { + case 'release': return 'ready'; + case 'IDLE': return 'ready'; // agent available after unanswered/canceled call + case 'calling': return 'calling'; + case 'incall': return 'in-call'; + case 'ACW': return 'acw'; + case 'logout': return 'offline'; + case 'AUX': + // "changeMode" is the brief AUX during login — not a real pause + if (eventData === 'changeMode') return null; + if (eventData?.toLowerCase().includes('training')) return 'training'; + return 'break'; + case 'login': return null; // wait for release + default: return null; + } + } + + getAgentState(agentId: string): AgentStateEntry | null { + return this.agentStates.get(agentId) ?? null; + } + + emitForceLogout(agentId: string) { + this.logger.log(`[AGENT-STATE] Emitting force-logout for ${agentId}`); + this.agentStates.set(agentId, { state: 'offline', timestamp: new Date().toISOString() }); + // Use a special state so frontend can distinguish admin force-logout from normal Ozonetel logout + this.agentStateSubject.next({ agentId, state: 'force-logout' as any, timestamp: new Date().toISOString() }); } getActiveCalls(): ActiveCall[] { diff --git a/src/worklist/missed-call-webhook.controller.ts b/src/worklist/missed-call-webhook.controller.ts index 03ad05a..386e79f 100644 --- a/src/worklist/missed-call-webhook.controller.ts +++ b/src/worklist/missed-call-webhook.controller.ts @@ -2,6 +2,16 @@ import { Controller, Post, Body, Headers, Logger } from '@nestjs/common'; import { PlatformGraphqlService } from '../platform/platform-graphql.service'; import { ConfigService } from '@nestjs/config'; +// Ozonetel sends all timestamps in IST — convert to UTC for storage +function istToUtc(istDateStr: string | null): string | null { + if (!istDateStr) return null; + // Parse as-is, then subtract 5:30 to get UTC + const d = new Date(istDateStr); + if (isNaN(d.getTime())) return null; + d.setMinutes(d.getMinutes() - 330); // IST is UTC+5:30 + return d.toISOString(); +} + @Controller('webhooks/ozonetel') export class MissedCallWebhookController { private readonly logger = new Logger(MissedCallWebhookController.name); @@ -130,8 +140,8 @@ export class MissedCallWebhookController { callStatus: data.callStatus, callerNumber: { primaryPhoneNumber: `+91${data.callerPhone}` }, agentName: data.agentName, - startedAt: data.startTime ? new Date(data.startTime).toISOString() : null, - endedAt: data.endTime ? new Date(data.endTime).toISOString() : null, + startedAt: istToUtc(data.startTime), + endedAt: istToUtc(data.endTime), durationSec: data.duration, disposition: this.mapDisposition(data.disposition), }; diff --git a/src/worklist/missed-queue.service.ts b/src/worklist/missed-queue.service.ts index 7083f9c..b2d613b 100644 --- a/src/worklist/missed-queue.service.ts +++ b/src/worklist/missed-queue.service.ts @@ -3,6 +3,15 @@ import { ConfigService } from '@nestjs/config'; import { PlatformGraphqlService } from '../platform/platform-graphql.service'; import { OzonetelAgentService } from '../ozonetel/ozonetel-agent.service'; +// Ozonetel sends all timestamps in IST — convert to UTC for storage +export function istToUtc(istDateStr: string | null): string | null { + if (!istDateStr) return null; + const d = new Date(istDateStr); + if (isNaN(d.getTime())) return null; + d.setMinutes(d.getMinutes() - 330); // IST is UTC+5:30 + return d.toISOString(); +} + // Normalize phone to +91XXXXXXXXXX format export function normalizePhone(raw: string): string { let digits = raw.replace(/[^0-9]/g, ''); @@ -61,9 +70,31 @@ export class MissedQueueService implements OnModuleInit { if (!phone || phone.length < 13) continue; const did = call.did || ''; - const callTime = call.callTime || new Date().toISOString(); + const callTime = istToUtc(call.callTime) ?? new Date().toISOString(); try { + // Look up lead by phone number — strip +91 prefix for flexible matching + const phoneDigits = phone.replace(/^\+91/, ''); + let leadId: string | null = null; + let leadName: string | null = null; + try { + const leadResult = await this.platform.query( + `{ leads(first: 1, filter: { + contactPhone: { primaryPhoneNumber: { like: "%${phoneDigits}" } } + }) { edges { node { id contactName { firstName lastName } patientId } } } }`, + ); + const matchedLead = leadResult?.leads?.edges?.[0]?.node; + if (matchedLead) { + leadId = matchedLead.id; + const fn = matchedLead.contactName?.firstName ?? ''; + const ln = matchedLead.contactName?.lastName ?? ''; + leadName = `${fn} ${ln}`.trim() || null; + this.logger.log(`Matched missed call ${phone} → lead ${leadId} (${leadName})`); + } + } catch (err) { + this.logger.warn(`Lead lookup failed for ${phone}: ${err}`); + } + const existing = await this.platform.query( `{ calls(first: 1, filter: { callbackstatus: { eq: PENDING_CALLBACK }, @@ -75,29 +106,35 @@ export class MissedQueueService implements OnModuleInit { if (existingNode) { const newCount = (existingNode.missedcallcount || 1) + 1; + const updateParts = [ + `missedcallcount: ${newCount}`, + `startedAt: "${callTime}"`, + `callsourcenumber: "${did}"`, + ]; + if (leadId) updateParts.push(`leadId: "${leadId}"`); + if (leadName) updateParts.push(`leadName: "${leadName}"`); await this.platform.query( - `mutation { updateCall(id: "${existingNode.id}", data: { - missedcallcount: ${newCount}, - startedAt: "${callTime}", - callsourcenumber: "${did}" - }) { id } }`, + `mutation { updateCall(id: "${existingNode.id}", data: { ${updateParts.join(', ')} }) { id } }`, ); updated++; - this.logger.log(`Dedup missed call ${phone}: count now ${newCount}`); + this.logger.log(`Dedup missed call ${phone}: count now ${newCount}${leadName ? ` (${leadName})` : ''}`); } else { + const dataParts = [ + `callStatus: MISSED`, + `direction: INBOUND`, + `callerNumber: { primaryPhoneNumber: "${phone}", primaryPhoneCallingCode: "+91" }`, + `callsourcenumber: "${did}"`, + `callbackstatus: PENDING_CALLBACK`, + `missedcallcount: 1`, + `startedAt: "${callTime}"`, + ]; + if (leadId) dataParts.push(`leadId: "${leadId}"`); + if (leadName) dataParts.push(`leadName: "${leadName}"`); await this.platform.query( - `mutation { createCall(data: { - callStatus: MISSED, - direction: INBOUND, - callerNumber: { primaryPhoneNumber: "${phone}", primaryPhoneCallingCode: "+91" }, - callsourcenumber: "${did}", - callbackstatus: PENDING_CALLBACK, - missedcallcount: 1, - startedAt: "${callTime}" - }) { id } }`, + `mutation { createCall(data: { ${dataParts.join(', ')} }) { id } }`, ); created++; - this.logger.log(`Created missed call record for ${phone}`); + this.logger.log(`Created missed call record for ${phone}${leadName ? ` → ${leadName}` : ''}`); } } catch (err) { this.logger.warn(`Failed to process abandon call ${ucid}: ${err}`); diff --git a/src/worklist/worklist.controller.ts b/src/worklist/worklist.controller.ts index de3b9d5..8fb771c 100644 --- a/src/worklist/worklist.controller.ts +++ b/src/worklist/worklist.controller.ts @@ -2,6 +2,7 @@ import { Controller, Get, Patch, Headers, Param, Body, HttpException, Logger } f import { PlatformGraphqlService } from '../platform/platform-graphql.service'; import { WorklistService } from './worklist.service'; import { MissedQueueService } from './missed-queue.service'; +import { SessionService } from '../auth/session.service'; @Controller('api/worklist') export class WorklistController { @@ -11,6 +12,7 @@ export class WorklistController { private readonly worklist: WorklistService, private readonly missedQueue: MissedQueueService, private readonly platform: PlatformGraphqlService, + private readonly session: SessionService, ) {} @Get() @@ -44,6 +46,12 @@ export class WorklistController { } private async resolveAgentName(authHeader: string): Promise { + // Check cached name from login (avoids currentUser query that CC agents can't access) + const token = authHeader.replace(/^Bearer\s+/i, ''); + const cached = await this.session.getCache(`agent:name:${token.slice(-16)}`); + if (cached) return cached; + + // Fallback: try querying platform (works for admin/supervisor tokens) try { const data = await this.platform.queryWithAuth( `{ currentUser { workspaceMember { name { firstName lastName } } } }`, @@ -54,7 +62,7 @@ export class WorklistController { const full = `${name?.firstName ?? ''} ${name?.lastName ?? ''}`.trim(); if (full) return full; } catch (err) { - this.logger.warn(`Failed to resolve agent name: ${err}`); + this.logger.warn(`Failed to resolve agent name via platform: ${err}`); } throw new HttpException('Could not determine agent identity', 400); } diff --git a/src/worklist/worklist.module.ts b/src/worklist/worklist.module.ts index cfc64c6..0b492c1 100644 --- a/src/worklist/worklist.module.ts +++ b/src/worklist/worklist.module.ts @@ -1,6 +1,8 @@ import { Module, forwardRef } from '@nestjs/common'; import { PlatformModule } from '../platform/platform.module'; import { OzonetelAgentModule } from '../ozonetel/ozonetel-agent.module'; +import { AuthModule } from '../auth/auth.module'; +import { RulesEngineModule } from '../rules-engine/rules-engine.module'; import { WorklistController } from './worklist.controller'; import { WorklistService } from './worklist.service'; import { MissedQueueService } from './missed-queue.service'; @@ -8,7 +10,7 @@ import { MissedCallWebhookController } from './missed-call-webhook.controller'; import { KookooCallbackController } from './kookoo-callback.controller'; @Module({ - imports: [PlatformModule, forwardRef(() => OzonetelAgentModule)], + imports: [PlatformModule, forwardRef(() => OzonetelAgentModule), forwardRef(() => AuthModule), RulesEngineModule], controllers: [WorklistController, MissedCallWebhookController, KookooCallbackController], providers: [WorklistService, MissedQueueService], exports: [MissedQueueService], diff --git a/src/worklist/worklist.service.ts b/src/worklist/worklist.service.ts index a93a323..00ededc 100644 --- a/src/worklist/worklist.service.ts +++ b/src/worklist/worklist.service.ts @@ -1,5 +1,6 @@ import { Injectable, Logger } from '@nestjs/common'; import { PlatformGraphqlService } from '../platform/platform-graphql.service'; +import { WorklistConsumer } from '../rules-engine/consumers/worklist.consumer'; export type WorklistResponse = { missedCalls: any[]; @@ -12,15 +13,33 @@ export type WorklistResponse = { export class WorklistService { private readonly logger = new Logger(WorklistService.name); - constructor(private readonly platform: PlatformGraphqlService) {} + constructor( + private readonly platform: PlatformGraphqlService, + private readonly worklistConsumer: WorklistConsumer, + ) {} async getWorklist(agentName: string, authHeader: string): Promise { - const [missedCalls, followUps, marketingLeads] = await Promise.all([ + const [rawMissedCalls, rawFollowUps, rawMarketingLeads] = await Promise.all([ this.getMissedCalls(agentName, authHeader), this.getPendingFollowUps(agentName, authHeader), this.getAssignedLeads(agentName, authHeader), ]); + // Tag each item with a type field for the scoring engine + const combined = [ + ...rawMissedCalls.map((item: any) => ({ ...item, type: 'missed' })), + ...rawFollowUps.map((item: any) => ({ ...item, type: 'follow-up' })), + ...rawMarketingLeads.map((item: any) => ({ ...item, type: 'lead' })), + ]; + + // Score and rank via rules engine + const scored = await this.worklistConsumer.scoreAndRank(combined); + + // Split back into the 3 categories + const missedCalls = scored.filter((item: any) => item.type === 'missed'); + const followUps = scored.filter((item: any) => item.type === 'follow-up'); + const marketingLeads = scored.filter((item: any) => item.type === 'lead'); + return { missedCalls, followUps,