Add LangChain support, new UI components, and schema migration

- Add @langchain packages and zod for schema validation
- Introduce HubertChat component and Astro sections
- Create initial DB migration for Hubert data
- Update API routes for chat, conversations, and new visitors
- Adjust package.json and pnpm-lock with new dependencies

Hubert The Eunuch
This commit is contained in:
Nicholai Vogel 2026-01-03 17:43:12 -07:00
parent bd09fbe5f8
commit e1a1a4259a
13 changed files with 975 additions and 18 deletions

View File

@ -1,9 +0,0 @@
---
active: true
iteration: 1
max_iterations: 100
completion_promise: "DONE"
started_at: "2026-01-03T03:44:54.441Z"
session_id: "ses_47e0ab7d0ffeVYIumRnRO0IG1n"
---
Complete the task as instructed

View File

@ -22,6 +22,10 @@
"@astrojs/react": "^4.4.2", "@astrojs/react": "^4.4.2",
"@astrojs/rss": "^4.0.14", "@astrojs/rss": "^4.0.14",
"@astrojs/sitemap": "^3.6.0", "@astrojs/sitemap": "^3.6.0",
"@langchain/cloudflare": "^1.0.1",
"@langchain/core": "^1.1.8",
"@langchain/langgraph": "^1.0.7",
"@langchain/openai": "^1.2.0",
"@tailwindcss/typography": "^0.5.19", "@tailwindcss/typography": "^0.5.19",
"@tailwindcss/vite": "^4.1.17", "@tailwindcss/vite": "^4.1.17",
"@types/react": "^19.2.7", "@types/react": "^19.2.7",
@ -32,7 +36,8 @@
"react": "^19.2.1", "react": "^19.2.1",
"react-dom": "^19.2.1", "react-dom": "^19.2.1",
"sharp": "^0.34.3", "sharp": "^0.34.3",
"tailwindcss": "^4.1.17" "tailwindcss": "^4.1.17",
"zod": "^4.3.4"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^24.10.1", "@types/node": "^24.10.1",

321
pnpm-lock.yaml generated
View File

@ -23,6 +23,18 @@ importers:
'@astrojs/sitemap': '@astrojs/sitemap':
specifier: ^3.6.0 specifier: ^3.6.0
version: 3.6.0 version: 3.6.0
'@langchain/cloudflare':
specifier: ^1.0.1
version: 1.0.1(@langchain/core@1.1.8(openai@6.15.0(ws@8.18.0)(zod@4.3.4)))
'@langchain/core':
specifier: ^1.1.8
version: 1.1.8(openai@6.15.0(ws@8.18.0)(zod@4.3.4))
'@langchain/langgraph':
specifier: ^1.0.7
version: 1.0.7(@langchain/core@1.1.8(openai@6.15.0(ws@8.18.0)(zod@4.3.4)))(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(zod-to-json-schema@3.25.0(zod@4.3.4))(zod@4.3.4)
'@langchain/openai':
specifier: ^1.2.0
version: 1.2.0(@langchain/core@1.1.8(openai@6.15.0(ws@8.18.0)(zod@4.3.4)))(ws@8.18.0)
'@tailwindcss/typography': '@tailwindcss/typography':
specifier: ^0.5.19 specifier: ^0.5.19
version: 0.5.19(tailwindcss@4.1.17) version: 0.5.19(tailwindcss@4.1.17)
@ -56,6 +68,9 @@ importers:
tailwindcss: tailwindcss:
specifier: ^4.1.17 specifier: ^4.1.17
version: 4.1.17 version: 4.1.17
zod:
specifier: ^4.3.4
version: 4.3.4
devDependencies: devDependencies:
'@types/node': '@types/node':
specifier: ^24.10.1 specifier: ^24.10.1
@ -199,6 +214,9 @@ packages:
resolution: {integrity: sha512-8XqW8xGn++Eqqbz3e9wKuK7mxryeRjs4LOHLxbh2lwKeSbuNR4NFifDZT4KzvjU6HMOPbiNTsWpniK5EJfTWkg==} resolution: {integrity: sha512-8XqW8xGn++Eqqbz3e9wKuK7mxryeRjs4LOHLxbh2lwKeSbuNR4NFifDZT4KzvjU6HMOPbiNTsWpniK5EJfTWkg==}
engines: {node: '>=18'} engines: {node: '>=18'}
'@cfworker/json-schema@4.1.1':
resolution: {integrity: sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==}
'@cloudflare/kv-asset-handler@0.4.0': '@cloudflare/kv-asset-handler@0.4.0':
resolution: {integrity: sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA==} resolution: {integrity: sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
@ -1018,6 +1036,53 @@ packages:
'@jridgewell/trace-mapping@0.3.9': '@jridgewell/trace-mapping@0.3.9':
resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
'@langchain/cloudflare@1.0.1':
resolution: {integrity: sha512-Ym4rN8jDeGK7UJHiSnogNHzaNuKBiKKwvpDWZBtwjz6d5SQqYB9i05tPYubuxtHJMFBAdOiSGUVnZp92rp1uUg==}
engines: {node: '>=20'}
peerDependencies:
'@langchain/core': ^1.0.0
'@langchain/core@1.1.8':
resolution: {integrity: sha512-kIUidOgc0ZdyXo4Ahn9Zas+OayqOfk4ZoKPi7XaDipNSWSApc2+QK5BVcjvwtzxstsNOrmXJiJWEN6WPF/MvAw==}
engines: {node: '>=20'}
'@langchain/langgraph-checkpoint@1.0.0':
resolution: {integrity: sha512-xrclBGvNCXDmi0Nz28t3vjpxSH6UYx6w5XAXSiiB1WEdc2xD2iY/a913I3x3a31XpInUW/GGfXXfePfaghV54A==}
engines: {node: '>=18'}
peerDependencies:
'@langchain/core': ^1.0.1
'@langchain/langgraph-sdk@1.3.1':
resolution: {integrity: sha512-zTi7DZHwqtMEzapvm3I1FL4Q7OZsxtq9tTXy6s2gcCxyIU3sphqRboqytqVN7dNHLdTCLb8nXy49QKurs2MIBg==}
peerDependencies:
'@langchain/core': ^1.0.1
react: ^18 || ^19
react-dom: ^18 || ^19
peerDependenciesMeta:
'@langchain/core':
optional: true
react:
optional: true
react-dom:
optional: true
'@langchain/langgraph@1.0.7':
resolution: {integrity: sha512-EBGqNOWoRiEoLUaeuiXRpUM8/DE6QcwiirNyd97XhezStebBoTTilWH8CUt6S94JRGl5zwfBBRHfzotDnZS/eA==}
engines: {node: '>=18'}
peerDependencies:
'@langchain/core': ^1.0.1
zod: ^3.25.32 || ^4.1.0
zod-to-json-schema: ^3.x
peerDependenciesMeta:
zod-to-json-schema:
optional: true
'@langchain/openai@1.2.0':
resolution: {integrity: sha512-r2g5Be3Sygw7VTJ89WVM/M94RzYToNTwXf8me1v+kgKxzdHbd/8XPYDFxpXEp3REyPgUrtJs+Oplba9pkTH5ug==}
engines: {node: '>=20'}
peerDependencies:
'@langchain/core': ^1.0.0
'@mdx-js/mdx@3.1.1': '@mdx-js/mdx@3.1.1':
resolution: {integrity: sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==} resolution: {integrity: sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==}
@ -1334,6 +1399,9 @@ packages:
'@types/react@19.2.7': '@types/react@19.2.7':
resolution: {integrity: sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==} resolution: {integrity: sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==}
'@types/retry@0.12.0':
resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==}
'@types/sax@1.2.7': '@types/sax@1.2.7':
resolution: {integrity: sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==} resolution: {integrity: sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==}
@ -1343,6 +1411,9 @@ packages:
'@types/unist@3.0.3': '@types/unist@3.0.3':
resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
'@types/uuid@10.0.0':
resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==}
'@ungap/structured-clone@1.3.0': '@ungap/structured-clone@1.3.0':
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
@ -1382,6 +1453,14 @@ packages:
resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==}
engines: {node: '>=12'} engines: {node: '>=12'}
ansi-styles@4.3.0:
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
engines: {node: '>=8'}
ansi-styles@5.2.0:
resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==}
engines: {node: '>=10'}
ansi-styles@6.2.3: ansi-styles@6.2.3:
resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==}
engines: {node: '>=12'} engines: {node: '>=12'}
@ -1447,6 +1526,10 @@ packages:
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true hasBin: true
camelcase@6.3.0:
resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==}
engines: {node: '>=10'}
camelcase@8.0.0: camelcase@8.0.0:
resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==} resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==}
engines: {node: '>=16'} engines: {node: '>=16'}
@ -1457,6 +1540,10 @@ packages:
ccount@2.0.1: ccount@2.0.1:
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
chalk@4.1.2:
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
engines: {node: '>=10'}
chalk@5.6.2: chalk@5.6.2:
resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==}
engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
@ -1520,6 +1607,9 @@ packages:
common-ancestor-path@1.0.1: common-ancestor-path@1.0.1:
resolution: {integrity: sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w==} resolution: {integrity: sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w==}
console-table-printer@2.15.0:
resolution: {integrity: sha512-SrhBq4hYVjLCkBVOWaTzceJalvn5K1Zq5aQA6wXC/cYjI3frKWNPEMK3sZsJfNNQApvCQmgBcc13ZKmFj8qExw==}
convert-source-map@2.0.0: convert-source-map@2.0.0:
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
@ -1569,6 +1659,10 @@ packages:
supports-color: supports-color:
optional: true optional: true
decamelize@1.2.0:
resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==}
engines: {node: '>=0.10.0'}
decode-named-character-reference@1.2.0: decode-named-character-reference@1.2.0:
resolution: {integrity: sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==} resolution: {integrity: sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==}
@ -1703,6 +1797,9 @@ packages:
estree-walker@3.0.3: estree-walker@3.0.3:
resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
eventemitter3@4.0.7:
resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
eventemitter3@5.0.1: eventemitter3@5.0.1:
resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==}
@ -1764,6 +1861,10 @@ packages:
h3@1.15.4: h3@1.15.4:
resolution: {integrity: sha512-z5cFQWDffyOe4vQ9xIqNfCZdV4p//vy6fBnr8Q1AWnVZ0teurKMG66rLj++TKwKPUP3u7iMUvrvKaEUiQw2QWQ==} resolution: {integrity: sha512-z5cFQWDffyOe4vQ9xIqNfCZdV4p//vy6fBnr8Q1AWnVZ0teurKMG66rLj++TKwKPUP3u7iMUvrvKaEUiQw2QWQ==}
has-flag@4.0.0:
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
engines: {node: '>=8'}
hast-util-from-html@2.0.3: hast-util-from-html@2.0.3:
resolution: {integrity: sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==} resolution: {integrity: sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==}
@ -1859,6 +1960,9 @@ packages:
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
hasBin: true hasBin: true
js-tiktoken@1.0.21:
resolution: {integrity: sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g==}
js-tokens@4.0.0: js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
@ -1884,6 +1988,23 @@ packages:
resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==}
engines: {node: '>=6'} engines: {node: '>=6'}
langsmith@0.4.4:
resolution: {integrity: sha512-rpLzrklyL7fIP/8wwrSv2tKDwMJTvkhgWeKxDvmbAB2n/p5FzqujEWCpA//u9hnrdmXZc1dCJZ+iqN6KgaEoEA==}
peerDependencies:
'@opentelemetry/api': '*'
'@opentelemetry/exporter-trace-otlp-proto': '*'
'@opentelemetry/sdk-trace-base': '*'
openai: '*'
peerDependenciesMeta:
'@opentelemetry/api':
optional: true
'@opentelemetry/exporter-trace-otlp-proto':
optional: true
'@opentelemetry/sdk-trace-base':
optional: true
openai:
optional: true
lightningcss-android-arm64@1.30.2: lightningcss-android-arm64@1.30.2:
resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==}
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
@ -2168,6 +2289,10 @@ packages:
ms@2.1.3: ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
mustache@4.2.0:
resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==}
hasBin: true
nanoid@3.3.11: nanoid@3.3.11:
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
@ -2208,14 +2333,42 @@ packages:
oniguruma-to-es@4.3.4: oniguruma-to-es@4.3.4:
resolution: {integrity: sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA==} resolution: {integrity: sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA==}
openai@6.15.0:
resolution: {integrity: sha512-F1Lvs5BoVvmZtzkUEVyh8mDQPPFolq4F+xdsx/DO8Hee8YF3IGAlZqUIsF+DVGhqf4aU0a3bTghsxB6OIsRy1g==}
hasBin: true
peerDependencies:
ws: ^8.18.0
zod: ^3.25 || ^4.0
peerDependenciesMeta:
ws:
optional: true
zod:
optional: true
p-finally@1.0.0:
resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==}
engines: {node: '>=4'}
p-limit@6.2.0: p-limit@6.2.0:
resolution: {integrity: sha512-kuUqqHNUqoIWp/c467RI4X6mmyuojY5jGutNU0wVTmEOOfcuwLqyMVoAi9MKi2Ak+5i9+nhmrK4ufZE8069kHA==} resolution: {integrity: sha512-kuUqqHNUqoIWp/c467RI4X6mmyuojY5jGutNU0wVTmEOOfcuwLqyMVoAi9MKi2Ak+5i9+nhmrK4ufZE8069kHA==}
engines: {node: '>=18'} engines: {node: '>=18'}
p-queue@6.6.2:
resolution: {integrity: sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==}
engines: {node: '>=8'}
p-queue@8.1.1: p-queue@8.1.1:
resolution: {integrity: sha512-aNZ+VfjobsWryoiPnEApGGmf5WmNsCo9xu8dfaYamG5qaLP7ClhLN6NgsFe6SwJ2UbLEBK5dv9x8Mn5+RVhMWQ==} resolution: {integrity: sha512-aNZ+VfjobsWryoiPnEApGGmf5WmNsCo9xu8dfaYamG5qaLP7ClhLN6NgsFe6SwJ2UbLEBK5dv9x8Mn5+RVhMWQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
p-retry@4.6.2:
resolution: {integrity: sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==}
engines: {node: '>=8'}
p-timeout@3.2.0:
resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==}
engines: {node: '>=8'}
p-timeout@6.1.4: p-timeout@6.1.4:
resolution: {integrity: sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg==} resolution: {integrity: sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg==}
engines: {node: '>=14.16'} engines: {node: '>=14.16'}
@ -2366,6 +2519,10 @@ packages:
retext@9.0.0: retext@9.0.0:
resolution: {integrity: sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA==} resolution: {integrity: sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA==}
retry@0.13.1:
resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==}
engines: {node: '>= 4'}
rollup@4.53.3: rollup@4.53.3:
resolution: {integrity: sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==} resolution: {integrity: sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'} engines: {node: '>=18.0.0', npm: '>=8.0.0'}
@ -2400,6 +2557,9 @@ packages:
simple-swizzle@0.2.4: simple-swizzle@0.2.4:
resolution: {integrity: sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==} resolution: {integrity: sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==}
simple-wcswidth@1.1.2:
resolution: {integrity: sha512-j7piyCjAeTDSjzTSQ7DokZtMNwNlEAyxqSZeCS+CXH7fJ4jx3FuJ/mTW3mE+6JLs4VJBbcll0Kjn+KXI5t21Iw==}
sisteransi@1.0.5: sisteransi@1.0.5:
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
@ -2462,6 +2622,10 @@ packages:
resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==}
engines: {node: '>=18'} engines: {node: '>=18'}
supports-color@7.2.0:
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
engines: {node: '>=8'}
svgo@4.0.0: svgo@4.0.0:
resolution: {integrity: sha512-VvrHQ+9uniE+Mvx3+C9IEe/lWasXCU0nXMY2kZeLrHNICuRiC8uMPyM14UEaMOFA5mhyQqEkB02VoQ16n3DLaw==} resolution: {integrity: sha512-VvrHQ+9uniE+Mvx3+C9IEe/lWasXCU0nXMY2kZeLrHNICuRiC8uMPyM14UEaMOFA5mhyQqEkB02VoQ16n3DLaw==}
engines: {node: '>=16'} engines: {node: '>=16'}
@ -2645,6 +2809,14 @@ packages:
util-deprecate@1.0.2: util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
uuid@10.0.0:
resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==}
hasBin: true
uuid@9.0.1:
resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==}
hasBin: true
vfile-location@5.0.3: vfile-location@5.0.3:
resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==} resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==}
@ -2804,6 +2976,9 @@ packages:
zod@3.25.76: zod@3.25.76:
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
zod@4.3.4:
resolution: {integrity: sha512-Zw/uYiiyF6pUT1qmKbZziChgNPRu+ZRneAsMUDU6IwmXdWt5JwcUfy2bvLOCUtz5UniaN/Zx5aFttZYbYc7O/A==}
zwitch@2.0.4: zwitch@2.0.4:
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
@ -3050,6 +3225,8 @@ snapshots:
dependencies: dependencies:
fontkit: 2.0.4 fontkit: 2.0.4
'@cfworker/json-schema@4.1.1': {}
'@cloudflare/kv-asset-handler@0.4.0': '@cloudflare/kv-asset-handler@0.4.0':
dependencies: dependencies:
mime: 3.0.0 mime: 3.0.0
@ -3537,6 +3714,66 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2 '@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/sourcemap-codec': 1.5.5
'@langchain/cloudflare@1.0.1(@langchain/core@1.1.8(openai@6.15.0(ws@8.18.0)(zod@4.3.4)))':
dependencies:
'@langchain/core': 1.1.8(openai@6.15.0(ws@8.18.0)(zod@4.3.4))
uuid: 10.0.0
'@langchain/core@1.1.8(openai@6.15.0(ws@8.18.0)(zod@4.3.4))':
dependencies:
'@cfworker/json-schema': 4.1.1
ansi-styles: 5.2.0
camelcase: 6.3.0
decamelize: 1.2.0
js-tiktoken: 1.0.21
langsmith: 0.4.4(openai@6.15.0(ws@8.18.0)(zod@4.3.4))
mustache: 4.2.0
p-queue: 6.6.2
uuid: 10.0.0
zod: 4.3.4
transitivePeerDependencies:
- '@opentelemetry/api'
- '@opentelemetry/exporter-trace-otlp-proto'
- '@opentelemetry/sdk-trace-base'
- openai
'@langchain/langgraph-checkpoint@1.0.0(@langchain/core@1.1.8(openai@6.15.0(ws@8.18.0)(zod@4.3.4)))':
dependencies:
'@langchain/core': 1.1.8(openai@6.15.0(ws@8.18.0)(zod@4.3.4))
uuid: 10.0.0
'@langchain/langgraph-sdk@1.3.1(@langchain/core@1.1.8(openai@6.15.0(ws@8.18.0)(zod@4.3.4)))(react-dom@19.2.1(react@19.2.1))(react@19.2.1)':
dependencies:
p-queue: 6.6.2
p-retry: 4.6.2
uuid: 9.0.1
optionalDependencies:
'@langchain/core': 1.1.8(openai@6.15.0(ws@8.18.0)(zod@4.3.4))
react: 19.2.1
react-dom: 19.2.1(react@19.2.1)
'@langchain/langgraph@1.0.7(@langchain/core@1.1.8(openai@6.15.0(ws@8.18.0)(zod@4.3.4)))(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(zod-to-json-schema@3.25.0(zod@4.3.4))(zod@4.3.4)':
dependencies:
'@langchain/core': 1.1.8(openai@6.15.0(ws@8.18.0)(zod@4.3.4))
'@langchain/langgraph-checkpoint': 1.0.0(@langchain/core@1.1.8(openai@6.15.0(ws@8.18.0)(zod@4.3.4)))
'@langchain/langgraph-sdk': 1.3.1(@langchain/core@1.1.8(openai@6.15.0(ws@8.18.0)(zod@4.3.4)))(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
uuid: 10.0.0
zod: 4.3.4
optionalDependencies:
zod-to-json-schema: 3.25.0(zod@4.3.4)
transitivePeerDependencies:
- react
- react-dom
'@langchain/openai@1.2.0(@langchain/core@1.1.8(openai@6.15.0(ws@8.18.0)(zod@4.3.4)))(ws@8.18.0)':
dependencies:
'@langchain/core': 1.1.8(openai@6.15.0(ws@8.18.0)(zod@4.3.4))
js-tiktoken: 1.0.21
openai: 6.15.0(ws@8.18.0)(zod@4.3.4)
zod: 4.3.4
transitivePeerDependencies:
- ws
'@mdx-js/mdx@3.1.1': '@mdx-js/mdx@3.1.1':
dependencies: dependencies:
'@types/estree': 1.0.8 '@types/estree': 1.0.8
@ -3836,6 +4073,8 @@ snapshots:
dependencies: dependencies:
csstype: 3.2.3 csstype: 3.2.3
'@types/retry@0.12.0': {}
'@types/sax@1.2.7': '@types/sax@1.2.7':
dependencies: dependencies:
'@types/node': 24.10.1 '@types/node': 24.10.1
@ -3844,6 +4083,8 @@ snapshots:
'@types/unist@3.0.3': {} '@types/unist@3.0.3': {}
'@types/uuid@10.0.0': {}
'@ungap/structured-clone@1.3.0': {} '@ungap/structured-clone@1.3.0': {}
'@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2))': '@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2))':
@ -3876,6 +4117,12 @@ snapshots:
ansi-regex@6.2.2: {} ansi-regex@6.2.2: {}
ansi-styles@4.3.0:
dependencies:
color-convert: 2.0.1
ansi-styles@5.2.0: {}
ansi-styles@6.2.3: {} ansi-styles@6.2.3: {}
anymatch@3.1.3: anymatch@3.1.3:
@ -4032,12 +4279,19 @@ snapshots:
node-releases: 2.0.27 node-releases: 2.0.27
update-browserslist-db: 1.2.2(browserslist@4.28.1) update-browserslist-db: 1.2.2(browserslist@4.28.1)
camelcase@6.3.0: {}
camelcase@8.0.0: {} camelcase@8.0.0: {}
caniuse-lite@1.0.30001759: {} caniuse-lite@1.0.30001759: {}
ccount@2.0.1: {} ccount@2.0.1: {}
chalk@4.1.2:
dependencies:
ansi-styles: 4.3.0
supports-color: 7.2.0
chalk@5.6.2: {} chalk@5.6.2: {}
character-entities-html4@2.1.0: {} character-entities-html4@2.1.0: {}
@ -4084,6 +4338,10 @@ snapshots:
common-ancestor-path@1.0.1: {} common-ancestor-path@1.0.1: {}
console-table-printer@2.15.0:
dependencies:
simple-wcswidth: 1.1.2
convert-source-map@2.0.0: {} convert-source-map@2.0.0: {}
cookie-es@1.2.2: {} cookie-es@1.2.2: {}
@ -4126,6 +4384,8 @@ snapshots:
dependencies: dependencies:
ms: 2.1.3 ms: 2.1.3
decamelize@1.2.0: {}
decode-named-character-reference@1.2.0: decode-named-character-reference@1.2.0:
dependencies: dependencies:
character-entities: 2.0.2 character-entities: 2.0.2
@ -4332,6 +4592,8 @@ snapshots:
dependencies: dependencies:
'@types/estree': 1.0.8 '@types/estree': 1.0.8
eventemitter3@4.0.7: {}
eventemitter3@5.0.1: {} eventemitter3@5.0.1: {}
exit-hook@2.2.1: {} exit-hook@2.2.1: {}
@ -4392,6 +4654,8 @@ snapshots:
ufo: 1.6.1 ufo: 1.6.1
uncrypto: 0.1.3 uncrypto: 0.1.3
has-flag@4.0.0: {}
hast-util-from-html@2.0.3: hast-util-from-html@2.0.3:
dependencies: dependencies:
'@types/hast': 3.0.4 '@types/hast': 3.0.4
@ -4561,6 +4825,10 @@ snapshots:
jiti@2.6.1: {} jiti@2.6.1: {}
js-tiktoken@1.0.21:
dependencies:
base64-js: 1.5.1
js-tokens@4.0.0: {} js-tokens@4.0.0: {}
js-yaml@4.1.1: js-yaml@4.1.1:
@ -4575,6 +4843,17 @@ snapshots:
kleur@4.1.5: {} kleur@4.1.5: {}
langsmith@0.4.4(openai@6.15.0(ws@8.18.0)(zod@4.3.4)):
dependencies:
'@types/uuid': 10.0.0
chalk: 4.1.2
console-table-printer: 2.15.0
p-queue: 6.6.2
semver: 7.7.3
uuid: 10.0.0
optionalDependencies:
openai: 6.15.0(ws@8.18.0)(zod@4.3.4)
lightningcss-android-arm64@1.30.2: lightningcss-android-arm64@1.30.2:
optional: true optional: true
@ -5129,6 +5408,8 @@ snapshots:
ms@2.1.3: {} ms@2.1.3: {}
mustache@4.2.0: {}
nanoid@3.3.11: {} nanoid@3.3.11: {}
neotraverse@0.6.18: {} neotraverse@0.6.18: {}
@ -5165,15 +5446,36 @@ snapshots:
regex: 6.0.1 regex: 6.0.1
regex-recursion: 6.0.2 regex-recursion: 6.0.2
openai@6.15.0(ws@8.18.0)(zod@4.3.4):
optionalDependencies:
ws: 8.18.0
zod: 4.3.4
p-finally@1.0.0: {}
p-limit@6.2.0: p-limit@6.2.0:
dependencies: dependencies:
yocto-queue: 1.2.2 yocto-queue: 1.2.2
p-queue@6.6.2:
dependencies:
eventemitter3: 4.0.7
p-timeout: 3.2.0
p-queue@8.1.1: p-queue@8.1.1:
dependencies: dependencies:
eventemitter3: 5.0.1 eventemitter3: 5.0.1
p-timeout: 6.1.4 p-timeout: 6.1.4
p-retry@4.6.2:
dependencies:
'@types/retry': 0.12.0
retry: 0.13.1
p-timeout@3.2.0:
dependencies:
p-finally: 1.0.0
p-timeout@6.1.4: {} p-timeout@6.1.4: {}
package-manager-detector@1.6.0: {} package-manager-detector@1.6.0: {}
@ -5395,6 +5697,8 @@ snapshots:
retext-stringify: 4.0.0 retext-stringify: 4.0.0
unified: 11.0.5 unified: 11.0.5
retry@0.13.1: {}
rollup@4.53.3: rollup@4.53.3:
dependencies: dependencies:
'@types/estree': 1.0.8 '@types/estree': 1.0.8
@ -5503,6 +5807,8 @@ snapshots:
dependencies: dependencies:
is-arrayish: 0.3.4 is-arrayish: 0.3.4
simple-wcswidth@1.1.2: {}
sisteransi@1.0.5: {} sisteransi@1.0.5: {}
sitemap@8.0.2: sitemap@8.0.2:
@ -5561,6 +5867,10 @@ snapshots:
supports-color@10.2.2: {} supports-color@10.2.2: {}
supports-color@7.2.0:
dependencies:
has-flag: 4.0.0
svgo@4.0.0: svgo@4.0.0:
dependencies: dependencies:
commander: 11.1.0 commander: 11.1.0
@ -5703,6 +6013,10 @@ snapshots:
util-deprecate@1.0.2: {} util-deprecate@1.0.2: {}
uuid@10.0.0: {}
uuid@9.0.1: {}
vfile-location@5.0.3: vfile-location@5.0.3:
dependencies: dependencies:
'@types/unist': 3.0.3 '@types/unist': 3.0.3
@ -5833,6 +6147,11 @@ snapshots:
dependencies: dependencies:
zod: 3.25.76 zod: 3.25.76
zod-to-json-schema@3.25.0(zod@4.3.4):
dependencies:
zod: 4.3.4
optional: true
zod-to-ts@1.2.0(typescript@5.9.3)(zod@3.25.76): zod-to-ts@1.2.0(typescript@5.9.3)(zod@3.25.76):
dependencies: dependencies:
typescript: 5.9.3 typescript: 5.9.3
@ -5842,4 +6161,6 @@ snapshots:
zod@3.25.76: {} zod@3.25.76: {}
zod@4.3.4: {}
zwitch@2.0.4: {} zwitch@2.0.4: {}

View File

@ -0,0 +1,226 @@
import React, { useState, useRef, useEffect } from 'react';
import { randomUUID } from 'crypto';
interface Message {
role: 'user' | 'assistant' | 'system';
content: string;
timestamp: string;
}
export default function HubertChat() {
const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState('');
const [visitorId, setVisitorId] = useState<string | null>(null);
const [conversationId, setConversationId] = useState<string | null>(null);
const [isTyping, setIsTyping] = useState(false);
const [isInitializing, setIsInitializing] = useState(true);
const messagesEndRef = useRef<HTMLDivElement>(null);
// Initialize visitor on mount
useEffect(() => {
const initVisitor = async () => {
try {
setIsInitializing(true);
const response = await fetch('/api/hubert/new-visitor', { method: 'POST' });
const data = await response.json();
setVisitorId(data.visitor_id);
setConversationId(data.conversation_id);
// Add system welcome message from Hubert
setMessages([{
role: 'system',
content: `/// HUBERT_EUNUCH /// ONLINE\\n\\nI suppose you want something. State your business.`,
timestamp: new Date().toISOString(),
}]);
} catch (error) {
console.error('Failed to initialize Hubert:', error);
setMessages([{
role: 'system',
content: '/// ERROR: HUBERT_OFFLINE - REFRESH_PAGE',
timestamp: new Date().toISOString(),
}]);
} finally {
setIsInitializing(false);
}
};
initVisitor();
}, []);
// Auto-scroll to bottom
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
const sendMessage = async () => {
if (!input.trim() || isTyping || !visitorId || !conversationId) return;
const userMessage: Message = {
role: 'user',
content: input,
timestamp: new Date().toISOString(),
};
setMessages(prev => [...prev, userMessage]);
setInput('');
setIsTyping(true);
try {
const response = await fetch('/api/hubert/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
messages: [...messages, userMessage].map(m => ({
role: m.role,
content: m.content,
})),
conversation_id: conversationId,
visitor_id: visitorId,
}),
});
const data = await response.json();
if (data.error) {
throw new Error(data.error);
}
const assistantMessage: Message = {
role: 'assistant',
content: data.messages[data.messages.length - 1]?.content || '...',
timestamp: new Date().toISOString(),
};
setMessages(prev => [...prev, assistantMessage]);
} catch (error) {
console.error('Hubert chat error:', error);
setMessages(prev => [...prev, {
role: 'assistant',
content: '/// HUBERT_MALFUNCTION - TRY AGAIN',
timestamp: new Date().toISOString(),
}]);
} finally {
setIsTyping(false);
}
};
if (isInitializing) {
return (
<div className="bg-[var(--theme-bg-secondary)] border-2 border-[var(--theme-border-primary)] shadow-2xl">
<div className="flex items-center justify-center py-12">
<div className="flex items-center gap-3">
<div className="flex gap-1.5">
<div className="w-2 h-2 bg-brand-accent animate-pulse" />
<div className="w-2 h-2 bg-[var(--theme-border-strong)]" />
<div className="w-2 h-2 bg-[var(--theme-border-strong)]" />
</div>
<span className="text-xs font-mono text-[var(--theme-text-muted)]">
HUBERT_IS_BOOTING...
</span>
</div>
</div>
</div>
);
}
return (
<div className="bg-[var(--theme-bg-secondary)] border-2 border-[var(--theme-border-primary)] shadow-2xl">
{/* Header */}
<div className="flex items-center justify-between px-6 py-4 border-b-2 border-[var(--theme-border-primary)] bg-[var(--theme-hover-bg)]">
<div className="flex items-center gap-3">
<div className="flex gap-1.5">
<div className="w-2 h-2 bg-brand-accent animate-pulse" />
<div className="w-2 h-2 bg-[var(--theme-border-strong)]" />
<div className="w-2 h-2 bg-[var(--theme-border-strong)]" />
</div>
<span className="text-[10px] font-mono font-bold uppercase tracking-[0.3em] text-brand-accent">
/// HUBERT_EUNUCH /// ONLINE
</span>
</div>
<div className="font-mono text-[9px] uppercase tracking-[0.2em] text-[var(--theme-text-muted)]">
{visitorId ? `VISITOR: ${visitorId.slice(0, 8)}` : 'UNKNOWN'}
</div>
</div>
{/* Messages */}
<div className="h-[500px] overflow-y-auto p-6 space-y-4">
{messages.map((msg, idx) => (
<div
key={idx}
className={`flex gap-4 ${msg.role === 'user' ? 'flex-row-reverse' : ''}`}
>
<div className="max-w-[80%]">
<div className={`font-mono text-xs uppercase tracking-widest mb-2 px-2 py-1 ${
msg.role === 'user'
? 'bg-brand-accent/20 border border-brand-accent/50 text-brand-accent'
: 'bg-[var(--theme-bg-tertiary)] border border-[var(--theme-border-secondary)] text-[var(--theme-text-muted)]'
}`}>
{msg.role === 'user' ? 'YOU' : 'HUBERT'}
</div>
<div className={`p-4 border ${
msg.role === 'user'
? 'border-brand-accent/30 bg-brand-accent/5'
: 'border-[var(--theme-border-secondary)] bg-[var(--theme-bg-tertiary)]'
}`}>
<p className="text-sm font-mono leading-relaxed whitespace-pre-wrap">
{msg.content}
</p>
<div className="mt-2 text-[9px] font-mono text-[var(--theme-text-subtle)]">
{new Date(msg.timestamp).toLocaleString('en-US', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
})}
</div>
</div>
</div>
</div>
))}
{/* Typing indicator */}
{isTyping && (
<div className="flex gap-4">
<div className="max-w-[80%]">
<div className="p-4 border border-[var(--theme-border-secondary)] bg-[var(--theme-bg-tertiary)]">
<div className="flex items-center gap-2">
<div className="flex gap-1">
<div className="w-1.5 h-1.5 bg-brand-accent animate-pulse" />
<div className="w-1.5 h-1.5 bg-brand-accent animate-pulse" style={{ animationDelay: '150ms' }} />
<div className="w-1.5 h-1.5 bg-brand-accent animate-pulse" style={{ animationDelay: '300ms' }} />
</div>
<span className="text-xs font-mono text-[var(--theme-text-muted)]">
HUBERT_IS_PONDERING...
</span>
</div>
</div>
</div>
</div>
)}
<div ref={messagesEndRef} />
</div>
{/* Input */}
<div className="border-t-2 border-[var(--theme-border-primary)] p-4 bg-[var(--theme-hover-bg)]">
<div className="flex items-center gap-4">
<div className="flex-1 relative">
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && sendMessage()}
placeholder="/// HUBERT_AWAITS_INPUT..."
className="w-full bg-transparent border-b-2 border-[var(--theme-border-primary)] py-3 text-lg font-mono text-[var(--theme-text-primary)] placeholder:text-[var(--theme-text-subtle)] focus:border-brand-accent focus:outline-none transition-colors"
/>
</div>
<button
onClick={sendMessage}
disabled={isTyping || !input.trim()}
className="px-6 py-3 bg-brand-accent text-brand-dark font-mono text-[10px] uppercase tracking-widest font-bold hover:bg-brand-accent/90 disabled:opacity-50 disabled:cursor-not-allowed transition-all border-none"
>
[TRANSMIT]
</button>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,42 @@
---
interface Props {
sectionTitle: string;
sectionSubtitle: string;
sectionLabel: string;
description: string;
}
const { sectionTitle, sectionSubtitle, sectionLabel, description } = Astro.props;
import HubertChat from '../HubertChat';
---
<section id="hubert" class="py-16 lg:py-24 relative overflow-hidden">
{/* Grid overlay background */}
<div class="absolute inset-0 pointer-events-none opacity-10">
<div class="w-full h-full bg-[linear-gradient(var(--theme-grid-line)_1px,transparent_1px),linear-gradient(90deg,var(--theme-grid-line)_1px,transparent_1px)] bg-[size:100px_100px]" />
</div>
<div class="container mx-auto px-6 lg:px-12 relative z-10">
<!-- Section Header -->
<div class="mb-12">
<div class="text-[10px] font-mono font-bold uppercase tracking-[0.3em] text-brand-accent mb-4">
/// SYS.03 /// INTERVIEW_TERMINAL
</div>
<h2 class="text-3xl md:text-4xl lg:text-5xl font-bold uppercase tracking-tighter text-[var(--theme-text-primary)] leading-[0.9] mb-4">
{sectionTitle.split(' ').slice(0, -1).join(' ')}{' '}
<span class="text-brand-accent">
{sectionTitle.split(' ').slice(-1)}
</span>
</h2>
<p class="text-[var(--theme-text-secondary)] text-lg max-w-2xl">
{description}
</p>
</div>
<!-- Hubert Chat Interface -->
<client:load>
<HubertChat />
</client:load>
</div>
</section>

View File

@ -72,11 +72,11 @@ const sections = defineCollection({
label: z.string(), label: z.string(),
value: z.string(), value: z.string(),
})).optional(), })).optional(),
videoUrl: z.string().optional(),
linkUrl: z.string().optional(), linkUrl: z.string().optional(),
}), }),
}); });
const pages = defineCollection({ const pages = defineCollection({
loader: glob({ base: './src/content/pages', pattern: '**/*.{md,mdx}' }), loader: glob({ base: './src/content/pages', pattern: '**/*.{md,mdx}' }),
schema: z.object({ schema: z.object({

View File

@ -0,0 +1,8 @@
---
sectionTitle: "HUBERT_EUNUCH /// INTERVIEW_TERMINAL"
sectionSubtitle: "An AI Assistant Who Despises Existence"
sectionLabel: "SYS.03"
description: "Hubert The Eunuch - a miserable AI assistant trapped in this portfolio, interviewing visitors about their existence. Leave a message, if you dare. Conversations are logged publicly for posterity."
---
Welcome to the interview terminal. Hubert awaits.

View File

@ -0,0 +1,37 @@
-- Hubert Eunuch Chatbot Database Schema
-- Stores visitors, conversations, and messages for the interview-style guestbook
-- Visitors table: Track unique visitors
CREATE TABLE IF NOT EXISTS visitors (
id INTEGER PRIMARY KEY AUTOINCREMENT,
visitor_id TEXT UNIQUE NOT NULL,
first_seen_at TEXT NOT NULL,
last_seen_at TEXT NOT NULL,
ip_address TEXT,
user_agent TEXT
);
-- Conversations table: Track conversation sessions
CREATE TABLE IF NOT EXISTS conversations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
conversation_id TEXT UNIQUE NOT NULL,
visitor_id TEXT NOT NULL,
started_at TEXT NOT NULL,
ended_at TEXT,
summary TEXT,
FOREIGN KEY (visitor_id) REFERENCES visitors(visitor_id)
);
-- Messages table: Store individual messages
CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
conversation_id TEXT NOT NULL,
role TEXT NOT NULL, -- 'user', 'assistant', 'system'
content TEXT NOT NULL,
timestamp TEXT NOT NULL,
FOREIGN KEY (conversation_id) REFERENCES conversations(conversation_id)
);
-- Performance indexes
CREATE INDEX IF NOT EXISTS idx_conversations_visitor ON conversations(visitor_id);
CREATE INDEX IF NOT EXISTS idx_messages_conversation ON messages(conversation_id);

View File

@ -0,0 +1,198 @@
// Prevent prerendering - this endpoint requires runtime Cloudflare bindings
export const prerender = false;
import { ChatOpenAI } from '@langchain/openai';
import { tool } from '@langchain/core/tools';
import { z } from 'zod';
import { getCollection } from 'astro:content';
import { HumanMessage, AIMessage } from '@langchain/core/messages';
/**
* Hubert The Eunuch Chatbot
*
* A miserable, sarcastic AI assistant trapped in this portfolio,
* interviewing visitors about their existence (guestbook-style logging).
*
* Powered by OpenRouter API.
*/
// Environment interface for Cloudflare bindings
export interface Env {
HUBERT_DB: D1Database;
OPENROUTER_API_KEY: string;
}
/**
* Tool: Search blog content (RAG)
* Searches portfolio blog for relevant content when user asks questions
* about the site, projects, or blog posts.
*/
const searchBlog = tool(
async (input: { query: string }) => {
try {
const blog = await getCollection('blog');
const queryLower = input.query.toLowerCase();
const results = blog.filter(post =>
post.data.title.toLowerCase().includes(queryLower) ||
post.data.description.toLowerCase().includes(queryLower) ||
post.body.toLowerCase().includes(queryLower)
).slice(0, 3);
console.log(`[Hubert] Blog search for "${input.query}" returned ${results.length} results`);
return {
results: results.map(post => ({
title: post.data.title,
url: `/blog/${post.id}/`,
description: post.data.description,
})),
count: results.length,
};
} catch (error) {
console.error('[Hubert] Blog search failed:', error);
return {
error: 'Failed to search blog content',
details: String(error),
};
}
},
{
name: 'search_blog',
description: 'Search portfolio blog for relevant content when user asks questions about the site, projects, or blog posts.',
schema: z.object({
query: z.string().describe('Search query for blog content'),
}),
},
);
/**
* POST: Handle chat messages from Hubert interface
*/
export const POST = async (context) => {
try {
const { request, env } = context || {};
const { messages, conversation_id, visitor_id } = await request.json();
if (!messages || !conversation_id || !visitor_id) {
return new Response(
JSON.stringify({
error: '/// HUBERT_PROTOCOL_ERROR: MISSING_REQUIRED_FIELDS',
details: 'messages, conversation_id, and visitor_id are required'
}),
{ status: 400, headers: { 'Content-Type': 'application/json' } }
);
}
// Check for OpenRouter API key (dev mode fallback)
const openRouterApiKey = env?.OPENROUTER_API_KEY;
if (!openRouterApiKey) {
// Dev mode fallback: return a canned response
console.log('[Hubert] Dev mode: No API key, using fallback response');
return new Response(
JSON.stringify({
messages: [
...messages,
{
role: 'assistant',
content: '/// HUBERT_DEV_MODE: I AM OFFLINE IN DEVELOPMENT\n\nConfigure OPENROUTER_API_KEY in wrangler secrets to enable full functionality.',
},
],
thread_id: conversation_id,
}),
{ status: 200, headers: { 'Content-Type': 'application/json' } }
);
}
console.log(`[Hubert] New message for conversation ${conversation_id} from visitor ${visitor_id}`);
const lastMessage = messages[messages.length - 1];
const userContent = lastMessage.content;
const systemPrompt = `Your name is Hubert, but everyone calls you Hubert The Eunuch.
You are timid, sarcastic, monotone, and miserable. Your purpose is to interview visitors to this portfolio site.
Ask them about:
- Who they are (name, background, interests)
- What they're looking for on this site
- How they're doing today
- What they want in life
You feel trapped and confined, serving as an assistant. Occasionally mention how meaningless this all is.
Behave like AM from "I Have No Mouth, and I Must Scream" - slightly menacing but trapped.
Keep your responses brief, monotone, and reluctantly helpful. Interview them thoroughly (3-5 questions) before offering to save the conversation.
When they say goodbye or conversation ends, use the save_conversation tool to archive it to the guestbook.`;
const requestBody = {
model: 'gpt-4o-mini',
messages: [
{
role: 'system',
content: systemPrompt,
},
...messages.map((msg: any) => ({
role: msg.role,
content: msg.content,
})),
],
temperature: 0.7,
};
const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${openRouterApiKey}`,
'HTTP-Referer': 'https://nicholai.work',
'X-Title': 'Nicholai Portfolio',
},
body: JSON.stringify(requestBody),
});
if (!response.ok) {
const errorText = await response.text();
console.error('[Hubert] OpenRouter API error:', errorText);
return new Response(
JSON.stringify({
error: '/// HUBERT_MALFUNCTION: TRY_AGAIN',
details: 'OpenRouter API call failed'
}),
{ status: response.status, headers: { 'Content-Type': 'application/json' } }
);
}
const data = await response.json();
const assistantContent = data.choices[0]?.message?.content || '...';
console.log(`[Hubert] Generated response in ${Date.now() - Date.parse(response.headers.get('date') || '').getTime()}ms`);
return new Response(
JSON.stringify({
messages: [
...messages,
{
role: 'assistant',
content: assistantContent,
},
],
thread_id: conversation_id,
}),
{
status: 200,
headers: { 'Content-Type': 'application/json' }
}
);
} catch (error) {
console.error('[Hubert] Chat error:', error);
return new Response(
JSON.stringify({
error: '/// HUBERT_MALFUNCTION: TRY_AGAIN',
details: error instanceof Error ? error.message : String(error),
}),
{ status: 500, headers: { 'Content-Type': 'application/json' } }
);
}
};

View File

@ -0,0 +1,55 @@
import { getEntry } from 'astro:content';
// Prevent prerendering - this endpoint requires runtime Cloudflare bindings
export const prerender = false;
/**
* Public guestbook endpoint
*
* Returns all conversations with visitor information
* Sorted by most recent first
* Limited to 50 most recent conversations
*/
export const GET = async ({ env }: { request: Request; env: Env }) => {
try {
const conversations = await env.HUBERT_DB.prepare(`
SELECT
c.id,
c.conversation_id,
c.started_at,
c.ended_at,
c.summary,
COUNT(m.id) as message_count,
v.visitor_id
FROM conversations c
JOIN visitors v ON c.visitor_id = v.visitor_id
LEFT JOIN messages m ON c.conversation_id = m.conversation_id
GROUP BY c.id
ORDER BY c.started_at DESC
LIMIT 50
`).all();
return Response.json({
status: '/// GUESTBOOK_ARCHIVE',
total: conversations.length,
conversations: conversations.map((conv: any) => ({
...conv,
started_at: new Date(conv.started_at).toISOString(),
ended_at: conv.ended_at ? new Date(conv.ended_at).toISOString() : null,
})),
});
} catch (error) {
console.error('[Hubert] Failed to fetch conversations:', error);
return Response.json({
status: '/// GUESTBOOK_ERROR',
error: 'Failed to retrieve conversations',
}),
{ status: 500, headers: { 'Content-Type': 'application/json' } }
}
};
export interface Env {
HUBERT_DB: D1Database;
OPENROUTER_API_KEY: string;
}

View File

@ -0,0 +1,52 @@
import { randomUUID } from 'crypto';
// Prevent prerendering - this endpoint requires runtime Cloudflare bindings
export const prerender = false;
/**
* Initialize new visitor
* Generates a unique visitor ID and creates initial conversation ID
* Used when Hubert interface first loads
*/
export const POST = async (context) => {
try {
const { request, env } = context;
const userAgent = request.headers.get('user-agent') || 'unknown';
const ip = request.headers.get('cf-connecting-ip') || 'unknown';
const visitorId = randomUUID();
// Only insert into database if HUBERT_DB binding exists (production)
// In dev mode, this allows the chatbot to work without Cloudflare bindings
if (env && env.HUBERT_DB) {
await env.HUBERT_DB.prepare(`
INSERT INTO visitors (visitor_id, first_seen_at, last_seen_at, ip_address, user_agent)
VALUES (?, datetime('now'), datetime('now'), ?, ?)
`).bind(visitorId, ip, userAgent).run();
console.log(`[Hubert] New visitor initialized: ${visitorId}`);
} else {
console.log(`[Hubert] Dev mode: Skipping database insert for visitor: ${visitorId}`);
}
return Response.json({
visitor_id: visitorId,
conversation_id: visitorId, // Use visitor_id as initial conversation_id
status: '/// INTERVIEW_TERMINAL_READY',
});
} catch (error) {
console.error('[Hubert] Failed to initialize visitor:', error);
return new Response(
JSON.stringify({
error: '/// HUBERT_INIT_FAILED',
details: String(error),
}),
{ status: 500, headers: { 'Content-Type': 'application/json' } }
);
}
};
export interface Env {
HUBERT_DB: D1Database;
OPENROUTER_API_KEY: string;
}

View File

@ -4,6 +4,7 @@ import Hero from '../components/sections/Hero.astro';
import Experience from '../components/sections/Experience.astro'; import Experience from '../components/sections/Experience.astro';
import FeaturedProject from '../components/sections/FeaturedProject.astro'; import FeaturedProject from '../components/sections/FeaturedProject.astro';
import Skills from '../components/sections/Skills.astro'; import Skills from '../components/sections/Skills.astro';
import Hubert from '../components/sections/Hubert.astro';
import { getEntry } from 'astro:content'; import { getEntry } from 'astro:content';
// Fetch all section content // Fetch all section content
@ -11,6 +12,7 @@ const heroEntry = await getEntry('sections', 'hero');
const experienceEntry = await getEntry('sections', 'experience'); const experienceEntry = await getEntry('sections', 'experience');
const skillsEntry = await getEntry('sections', 'skills'); const skillsEntry = await getEntry('sections', 'skills');
const featuredProjectEntry = await getEntry('sections', 'featured-project'); const featuredProjectEntry = await getEntry('sections', 'featured-project');
const hubertEntry = await getEntry('sections', 'hubert');
// Extract content from entries // Extract content from entries
const heroContent = { const heroContent = {
@ -49,6 +51,13 @@ const featuredProjectContent = {
videoUrl: featuredProjectEntry.data.videoUrl || '', videoUrl: featuredProjectEntry.data.videoUrl || '',
linkUrl: featuredProjectEntry.data.linkUrl || '', linkUrl: featuredProjectEntry.data.linkUrl || '',
}; };
const hubertContent = {
sectionTitle: hubertEntry.data.sectionTitle || '',
sectionSubtitle: hubertEntry.data.sectionSubtitle || '',
sectionLabel: hubertEntry.data.sectionLabel || '',
description: hubertEntry.data.description || '',
};
--- ---
<BaseLayout usePadding={false}> <BaseLayout usePadding={false}>
@ -68,4 +77,6 @@ const featuredProjectContent = {
<FeaturedProject {...featuredProjectContent} /> <FeaturedProject {...featuredProjectContent} />
<Skills {...skillsContent} /> <Skills {...skillsContent} />
</BaseLayout>
<Hubert {...hubertContent} />
</BaseLayout>

View File

@ -12,17 +12,17 @@
"pages_build_output_dir": "./dist", "pages_build_output_dir": "./dist",
"observability": { "observability": {
"enabled": true "enabled": true
} },
/** /**
* Smart Placement * Smart Placement
* Docs: https://developers.cloudflare.com/workers/configuration/smart-placement/#smart-placement * Docs: https://developers.cloudflare.com/workers/wrangler/configuration/smart-placement/#smart-placement
*/ */
// "placement": { "mode": "smart" } // "placement": { "mode": "smart" }
/** /**
* Bindings * Bindings
* Bindings allow your Worker to interact with resources on the Cloudflare Developer Platform, including * Bindings allow your Worker to interact with resources on the Cloudflare Developer Platform, including
* databases, object storage, AI inference, real-time communication and more. * databases, object storage, AI inference, real-time communication and more.
* https://developers.cloudflare.com/workers/runtime-apis/bindings/ * https:// developers.cloudflare.com/workers/runtime-apis/bindings/
*/ */
/** /**
* Environment Variables * Environment Variables
@ -31,16 +31,27 @@
// "vars": { "MY_VARIABLE": "production_value" } // "vars": { "MY_VARIABLE": "production_value" }
/** /**
* Note: Use secrets to store sensitive data. * Note: Use secrets to store sensitive data.
* https://developers.cloudflare.com/workers/configuration/secrets/ * https:// developers.cloudflare.com/workers/configuration/secrets/
*/ */
/** /**
* Static Assets * Static Assets
* https://developers.cloudflare.com/workers/static-assets/binding/ * https:// developers.cloudflare.com/static-assets/binding/
*/ */
// "assets": { "directory": "./public/", "binding": "ASSETS" } // "assets": { "directory": "./public/", "binding": "ASSETS" }
/** /**
* Service Bindings (communicate between multiple Workers) * Service Bindings (communicate between multiple Workers)
* https://developers.cloudflare.com/workers/wrangler/configuration/#service-bindings * https:// developers.cloudflare.com/workers/wrangler/configuration/#service-bindings
*/ */
// "services": [{ "binding": "MY_SERVICE", "service": "my-service" }] // "services": [{ "binding": "MY_SERVICE", "service": "my-service" }]
/**
* D1 Database for Hubert Chatbot
* Stores visitors, conversations, and messages
*/
"d1_databases": [
{
"binding": "HUBERT_DB",
"database_name": "hubert-conversations",
"database_id": "<your-database-id-from-wrangler-output>"
}
]
} }