diff --git a/package.json b/package.json
index eae06e2..e47a8eb 100644
--- a/package.json
+++ b/package.json
@@ -16,7 +16,8 @@
"author": "",
"license": "ISC",
"devDependencies": {
- "@types/node": "^20.2.5",
+ "@octokit/types": "^9.3.0",
+ "@types/node": "^20.2.6",
"@types/prettier": "^2.7.3",
"tsc-watch": "^6.0.4",
"typescript": "^5.1.3"
@@ -27,10 +28,12 @@
"@effect/io": "^0.26.0",
"@effect/schema": "^0.20.1",
"@effect/stream": "^0.22.0",
- "dfx": "^0.48.0",
+ "dfx": "^0.48.1",
"dotenv": "^16.1.4",
"effect-schema-class": "^0.4.0",
+ "gpt-tokenizer": "^2.1.1",
"html-entities": "^2.3.5",
+ "octokit": "^2.0.19",
"openai": "^3.2.1",
"prettier": "^2.8.8"
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index a28a572..c72abd6 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -21,17 +21,23 @@ dependencies:
specifier: ^0.22.0
version: 0.22.0
dfx:
- specifier: ^0.48.0
- version: 0.48.0
+ specifier: ^0.48.1
+ version: 0.48.1
dotenv:
specifier: ^16.1.4
version: 16.1.4
effect-schema-class:
specifier: ^0.4.0
version: 0.4.0
+ gpt-tokenizer:
+ specifier: ^2.1.1
+ version: 2.1.1
html-entities:
specifier: ^2.3.5
version: 2.3.5
+ octokit:
+ specifier: ^2.0.19
+ version: 2.0.19
openai:
specifier: ^3.2.1
version: 3.2.1
@@ -40,9 +46,12 @@ dependencies:
version: 2.8.8
devDependencies:
+ '@octokit/types':
+ specifier: ^9.3.0
+ version: 9.3.0
'@types/node':
- specifier: ^20.2.5
- version: 20.2.5
+ specifier: ^20.2.6
+ version: 20.2.6
'@types/prettier':
specifier: ^2.7.3
version: 2.7.3
@@ -89,23 +98,284 @@ packages:
'@effect/io': 0.26.0
dev: false
+ /@octokit/app@13.1.5:
+ resolution: {integrity: sha512-6qTa24S+gdQUU66SCVfqTkyt2jAr9/ZeyPqJhnNI9PZ8Wum4lQy3bPS+voGlxABNOlzRKnxbSdYKoraMr3MqBA==}
+ engines: {node: '>= 14'}
+ dependencies:
+ '@octokit/auth-app': 4.0.13
+ '@octokit/auth-unauthenticated': 3.0.5
+ '@octokit/core': 4.2.1
+ '@octokit/oauth-app': 4.2.2
+ '@octokit/plugin-paginate-rest': 6.1.2(@octokit/core@4.2.1)
+ '@octokit/types': 9.3.0
+ '@octokit/webhooks': 10.9.1
+ transitivePeerDependencies:
+ - encoding
+ dev: false
+
+ /@octokit/auth-app@4.0.13:
+ resolution: {integrity: sha512-NBQkmR/Zsc+8fWcVIFrwDgNXS7f4XDrkd9LHdi9DPQw1NdGHLviLzRO2ZBwTtepnwHXW5VTrVU9eFGijMUqllg==}
+ engines: {node: '>= 14'}
+ dependencies:
+ '@octokit/auth-oauth-app': 5.0.6
+ '@octokit/auth-oauth-user': 2.1.2
+ '@octokit/request': 6.2.5
+ '@octokit/request-error': 3.0.3
+ '@octokit/types': 9.3.0
+ deprecation: 2.3.1
+ lru-cache: 9.1.2
+ universal-github-app-jwt: 1.1.1
+ universal-user-agent: 6.0.0
+ transitivePeerDependencies:
+ - encoding
+ dev: false
+
+ /@octokit/auth-oauth-app@5.0.6:
+ resolution: {integrity: sha512-SxyfIBfeFcWd9Z/m1xa4LENTQ3l1y6Nrg31k2Dcb1jS5ov7pmwMJZ6OGX8q3K9slRgVpeAjNA1ipOAMHkieqyw==}
+ engines: {node: '>= 14'}
+ dependencies:
+ '@octokit/auth-oauth-device': 4.0.5
+ '@octokit/auth-oauth-user': 2.1.2
+ '@octokit/request': 6.2.5
+ '@octokit/types': 9.3.0
+ '@types/btoa-lite': 1.0.0
+ btoa-lite: 1.0.0
+ universal-user-agent: 6.0.0
+ transitivePeerDependencies:
+ - encoding
+ dev: false
+
+ /@octokit/auth-oauth-device@4.0.5:
+ resolution: {integrity: sha512-XyhoWRTzf2ZX0aZ52a6Ew5S5VBAfwwx1QnC2Np6Et3MWQpZjlREIcbcvVZtkNuXp6Z9EeiSLSDUqm3C+aMEHzQ==}
+ engines: {node: '>= 14'}
+ dependencies:
+ '@octokit/oauth-methods': 2.0.6
+ '@octokit/request': 6.2.5
+ '@octokit/types': 9.3.0
+ universal-user-agent: 6.0.0
+ transitivePeerDependencies:
+ - encoding
+ dev: false
+
+ /@octokit/auth-oauth-user@2.1.2:
+ resolution: {integrity: sha512-kkRqNmFe7s5GQcojE3nSlF+AzYPpPv7kvP/xYEnE57584pixaFBH8Vovt+w5Y3E4zWUEOxjdLItmBTFAWECPAg==}
+ engines: {node: '>= 14'}
+ dependencies:
+ '@octokit/auth-oauth-device': 4.0.5
+ '@octokit/oauth-methods': 2.0.6
+ '@octokit/request': 6.2.5
+ '@octokit/types': 9.3.0
+ btoa-lite: 1.0.0
+ universal-user-agent: 6.0.0
+ transitivePeerDependencies:
+ - encoding
+ dev: false
+
+ /@octokit/auth-token@3.0.4:
+ resolution: {integrity: sha512-TWFX7cZF2LXoCvdmJWY7XVPi74aSY0+FfBZNSXEXFkMpjcqsQwDSYVv5FhRFaI0V1ECnwbz4j59T/G+rXNWaIQ==}
+ engines: {node: '>= 14'}
+ dev: false
+
+ /@octokit/auth-unauthenticated@3.0.5:
+ resolution: {integrity: sha512-yH2GPFcjrTvDWPwJWWCh0tPPtTL5SMgivgKPA+6v/XmYN6hGQkAto8JtZibSKOpf8ipmeYhLNWQ2UgW0GYILCw==}
+ engines: {node: '>= 14'}
+ dependencies:
+ '@octokit/request-error': 3.0.3
+ '@octokit/types': 9.3.0
+ dev: false
+
+ /@octokit/core@4.2.1:
+ resolution: {integrity: sha512-tEDxFx8E38zF3gT7sSMDrT1tGumDgsw5yPG6BBh/X+5ClIQfMH/Yqocxz1PnHx6CHyF6pxmovUTOfZAUvQ0Lvw==}
+ engines: {node: '>= 14'}
+ dependencies:
+ '@octokit/auth-token': 3.0.4
+ '@octokit/graphql': 5.0.6
+ '@octokit/request': 6.2.5
+ '@octokit/request-error': 3.0.3
+ '@octokit/types': 9.3.0
+ before-after-hook: 2.2.3
+ universal-user-agent: 6.0.0
+ transitivePeerDependencies:
+ - encoding
+ dev: false
+
+ /@octokit/endpoint@7.0.6:
+ resolution: {integrity: sha512-5L4fseVRUsDFGR00tMWD/Trdeeihn999rTMGRMC1G/Ldi1uWlWJzI98H4Iak5DB/RVvQuyMYKqSK/R6mbSOQyg==}
+ engines: {node: '>= 14'}
+ dependencies:
+ '@octokit/types': 9.3.0
+ is-plain-object: 5.0.0
+ universal-user-agent: 6.0.0
+ dev: false
+
+ /@octokit/graphql@5.0.6:
+ resolution: {integrity: sha512-Fxyxdy/JH0MnIB5h+UQ3yCoh1FG4kWXfFKkpWqjZHw/p+Kc8Y44Hu/kCgNBT6nU1shNumEchmW/sUO1JuQnPcw==}
+ engines: {node: '>= 14'}
+ dependencies:
+ '@octokit/request': 6.2.5
+ '@octokit/types': 9.3.0
+ universal-user-agent: 6.0.0
+ transitivePeerDependencies:
+ - encoding
+ dev: false
+
+ /@octokit/oauth-app@4.2.2:
+ resolution: {integrity: sha512-/jsPd43Yu2UXJ4XGq9KyOjPj5kNWQ5pfVzeDEfIVE8ENchyIPS+/IY2a8b0+OQSAsBKBLTHVp9m51RfGHmPZlw==}
+ engines: {node: '>= 14'}
+ dependencies:
+ '@octokit/auth-oauth-app': 5.0.6
+ '@octokit/auth-oauth-user': 2.1.2
+ '@octokit/auth-unauthenticated': 3.0.5
+ '@octokit/core': 4.2.1
+ '@octokit/oauth-authorization-url': 5.0.0
+ '@octokit/oauth-methods': 2.0.6
+ '@types/aws-lambda': 8.10.117
+ fromentries: 1.3.2
+ universal-user-agent: 6.0.0
+ transitivePeerDependencies:
+ - encoding
+ dev: false
+
+ /@octokit/oauth-authorization-url@5.0.0:
+ resolution: {integrity: sha512-y1WhN+ERDZTh0qZ4SR+zotgsQUE1ysKnvBt1hvDRB2WRzYtVKQjn97HEPzoehh66Fj9LwNdlZh+p6TJatT0zzg==}
+ engines: {node: '>= 14'}
+ dev: false
+
+ /@octokit/oauth-methods@2.0.6:
+ resolution: {integrity: sha512-l9Uml2iGN2aTWLZcm8hV+neBiFXAQ9+3sKiQe/sgumHlL6HDg0AQ8/l16xX/5jJvfxueqTW5CWbzd0MjnlfHZw==}
+ engines: {node: '>= 14'}
+ dependencies:
+ '@octokit/oauth-authorization-url': 5.0.0
+ '@octokit/request': 6.2.5
+ '@octokit/request-error': 3.0.3
+ '@octokit/types': 9.3.0
+ btoa-lite: 1.0.0
+ transitivePeerDependencies:
+ - encoding
+ dev: false
+
+ /@octokit/openapi-types@18.0.0:
+ resolution: {integrity: sha512-V8GImKs3TeQRxRtXFpG2wl19V7444NIOTDF24AWuIbmNaNYOQMWRbjcGDXV5B+0n887fgDcuMNOmlul+k+oJtw==}
+
+ /@octokit/plugin-paginate-rest@6.1.2(@octokit/core@4.2.1):
+ resolution: {integrity: sha512-qhrmtQeHU/IivxucOV1bbI/xZyC/iOBhclokv7Sut5vnejAIAEXVcGQeRpQlU39E0WwK9lNvJHphHri/DB6lbQ==}
+ engines: {node: '>= 14'}
+ peerDependencies:
+ '@octokit/core': '>=4'
+ dependencies:
+ '@octokit/core': 4.2.1
+ '@octokit/tsconfig': 1.0.2
+ '@octokit/types': 9.3.0
+ dev: false
+
+ /@octokit/plugin-rest-endpoint-methods@7.2.0(@octokit/core@4.2.1):
+ resolution: {integrity: sha512-yZhsKs8QnQbEpeAUd0WGgNJLoVHzENir7H0JmZg9BWRCZB+Fmqj7x/+sUFK7kw24XjWWVIQ4qGswps42/lmiIQ==}
+ engines: {node: '>= 14'}
+ peerDependencies:
+ '@octokit/core': '>=3'
+ dependencies:
+ '@octokit/core': 4.2.1
+ '@octokit/types': 9.3.0
+ dev: false
+
+ /@octokit/plugin-retry@4.1.6(@octokit/core@4.2.1):
+ resolution: {integrity: sha512-obkYzIgEC75r8+9Pnfiiqy3y/x1bc3QLE5B7qvv9wi9Kj0R5tGQFC6QMBg1154WQ9lAVypuQDGyp3hNpp15gQQ==}
+ engines: {node: '>= 14'}
+ peerDependencies:
+ '@octokit/core': '>=3'
+ dependencies:
+ '@octokit/core': 4.2.1
+ '@octokit/types': 9.3.0
+ bottleneck: 2.19.5
+ dev: false
+
+ /@octokit/plugin-throttling@5.2.3(@octokit/core@4.2.1):
+ resolution: {integrity: sha512-C9CFg9mrf6cugneKiaI841iG8DOv6P5XXkjmiNNut+swePxQ7RWEdAZRp5rJoE1hjsIqiYcKa/ZkOQ+ujPI39Q==}
+ engines: {node: '>= 14'}
+ peerDependencies:
+ '@octokit/core': ^4.0.0
+ dependencies:
+ '@octokit/core': 4.2.1
+ '@octokit/types': 9.3.0
+ bottleneck: 2.19.5
+ dev: false
+
+ /@octokit/request-error@3.0.3:
+ resolution: {integrity: sha512-crqw3V5Iy2uOU5Np+8M/YexTlT8zxCfI+qu+LxUB7SZpje4Qmx3mub5DfEKSO8Ylyk0aogi6TYdf6kxzh2BguQ==}
+ engines: {node: '>= 14'}
+ dependencies:
+ '@octokit/types': 9.3.0
+ deprecation: 2.3.1
+ once: 1.4.0
+ dev: false
+
+ /@octokit/request@6.2.5:
+ resolution: {integrity: sha512-z83E8UIlPNaJUsXpjD8E0V5o/5f+vJJNbNcBwVZsX3/vC650U41cOkTLjq4PKk9BYonQGOnx7N17gvLyNjgGcQ==}
+ engines: {node: '>= 14'}
+ dependencies:
+ '@octokit/endpoint': 7.0.6
+ '@octokit/request-error': 3.0.3
+ '@octokit/types': 9.3.0
+ is-plain-object: 5.0.0
+ node-fetch: 2.6.11
+ universal-user-agent: 6.0.0
+ transitivePeerDependencies:
+ - encoding
+ dev: false
+
+ /@octokit/tsconfig@1.0.2:
+ resolution: {integrity: sha512-I0vDR0rdtP8p2lGMzvsJzbhdOWy405HcGovrspJ8RRibHnyRgggUSNO5AIox5LmqiwmatHKYsvj6VGFHkqS7lA==}
+ dev: false
+
+ /@octokit/types@9.3.0:
+ resolution: {integrity: sha512-ZNO1In0QuWZLDngSLcn5H4ExRhYOd1rDcWnwc/LuR55cO1d6Sex6+T6RiSQwp/tyEg7eNWx+MUdJGL7Fu1kMjw==}
+ dependencies:
+ '@octokit/openapi-types': 18.0.0
+
+ /@octokit/webhooks-methods@3.0.3:
+ resolution: {integrity: sha512-2vM+DCNTJ5vL62O5LagMru6XnYhV4fJslK+5YUkTa6rWlW2S+Tqs1lF9Wr9OGqHfVwpBj3TeztWfVON/eUoW1Q==}
+ engines: {node: '>= 14'}
+ dev: false
+
+ /@octokit/webhooks-types@6.11.0:
+ resolution: {integrity: sha512-AanzbulOHljrku1NGfafxdpTCfw2ENaWzH01N2vqQM+cUFbk868Cgh0xylz0JIM9BoKbfI++bdD6EYX0Q/UTEw==}
+ dev: false
+
+ /@octokit/webhooks@10.9.1:
+ resolution: {integrity: sha512-5NXU4VfsNOo2VSU/SrLrpPH2Z1ZVDOWFcET4EpnEBX1uh/v8Uz65UVuHIRx5TZiXhnWyRE9AO1PXHa+M/iWwZA==}
+ engines: {node: '>= 14'}
+ dependencies:
+ '@octokit/request-error': 3.0.3
+ '@octokit/webhooks-methods': 3.0.3
+ '@octokit/webhooks-types': 6.11.0
+ aggregate-error: 3.1.0
+ dev: false
+
+ /@types/aws-lambda@8.10.117:
+ resolution: {integrity: sha512-6T1aHTSSK4l8+67ANKHha/CRVxyk/bAl6OGCOxsKVsHaSxWpqsqgupc8rPw8vQGjtIgIZ+EaHqMz8gA4d6xZhQ==}
+ dev: false
+
/@types/body-parser@1.19.2:
resolution: {integrity: sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==}
dependencies:
'@types/connect': 3.4.35
- '@types/node': 20.2.5
+ '@types/node': 20.2.6
+ dev: false
+
+ /@types/btoa-lite@1.0.0:
+ resolution: {integrity: sha512-wJsiX1tosQ+J5+bY5LrSahHxr2wT+uME5UDwdN1kg4frt40euqA+wzECkmq4t5QbveHiJepfdThgQrPw6KiSlg==}
dev: false
/@types/connect@3.4.35:
resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==}
dependencies:
- '@types/node': 20.2.5
+ '@types/node': 20.2.6
dev: false
/@types/express-serve-static-core@4.17.35:
resolution: {integrity: sha512-wALWQwrgiB2AWTT91CB62b6Yt0sNHpznUXeZEcnPU3DRdlDIz74x8Qg1UUYKSVFi+va5vKOLYRBI1bRKiLLKIg==}
dependencies:
- '@types/node': 20.2.5
+ '@types/node': 20.2.6
'@types/qs': 6.9.7
'@types/range-parser': 1.2.4
'@types/send': 0.17.1
@@ -120,6 +390,12 @@ packages:
'@types/serve-static': 1.15.1
dev: false
+ /@types/jsonwebtoken@9.0.2:
+ resolution: {integrity: sha512-drE6uz7QBKq1fYqqoFKTDRdFCPHd5TCub75BM+D+cMx7NU9hUz7SESLfC2fSCXVFMO5Yj8sOWHuGqPgjc+fz0Q==}
+ dependencies:
+ '@types/node': 20.2.6
+ dev: false
+
/@types/mime@1.3.2:
resolution: {integrity: sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==}
dev: false
@@ -128,8 +404,8 @@ packages:
resolution: {integrity: sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==}
dev: false
- /@types/node@20.2.5:
- resolution: {integrity: sha512-JJulVEQXmiY9Px5axXHeYGLSjhkZEnD+MDPDGbCbIAbMslkKwmygtZFy1X6s/075Yo94sf8GuSlFfPzysQrWZQ==}
+ /@types/node@20.2.6:
+ resolution: {integrity: sha512-GQBWUtGoefMEOx/vu+emHEHU5aw6JdDoEtZhoBrHFPZbA/YNRFfN996XbBASEWdvmLSLyv9FKYppYGyZjCaq/g==}
/@types/prettier@2.7.3:
resolution: {integrity: sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==}
@@ -147,14 +423,22 @@ packages:
resolution: {integrity: sha512-Cwo8LE/0rnvX7kIIa3QHCkcuF21c05Ayb0ZfxPiv0W8VRiZiNW/WuRupHKpqqGVGf7SUA44QSOUKaEd9lIrd/Q==}
dependencies:
'@types/mime': 1.3.2
- '@types/node': 20.2.5
+ '@types/node': 20.2.6
dev: false
/@types/serve-static@1.15.1:
resolution: {integrity: sha512-NUo5XNiAdULrJENtJXZZ3fHtfMolzZwczzBbnAeBbqBwG+LaG6YaJtuwzwGSQZ2wsCrxjEhNNjAkKigy3n8teQ==}
dependencies:
'@types/mime': 3.0.1
- '@types/node': 20.2.5
+ '@types/node': 20.2.6
+ dev: false
+
+ /aggregate-error@3.1.0:
+ resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==}
+ engines: {node: '>=8'}
+ dependencies:
+ clean-stack: 2.2.0
+ indent-string: 4.0.0
dev: false
/asynckit@0.4.0:
@@ -169,6 +453,22 @@ packages:
- debug
dev: false
+ /before-after-hook@2.2.3:
+ resolution: {integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==}
+ dev: false
+
+ /bottleneck@2.19.5:
+ resolution: {integrity: sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==}
+ dev: false
+
+ /btoa-lite@1.0.0:
+ resolution: {integrity: sha512-gvW7InbIyF8AicrqWoptdW08pUxuhq8BEgowNajy9RhiE86fmGAGl+bLKo6oB8QP0CkqHLowfN0oJdKC/J6LbA==}
+ dev: false
+
+ /buffer-equal-constant-time@1.0.1:
+ resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
+ dev: false
+
/bufferutil@4.0.7:
resolution: {integrity: sha512-kukuqc39WOHtdxtw4UScxF/WVnMFVSQVKhtx3AjZJzhd0RGZZldcrfSEbVsWWe6KNH253574cq5F+wpv0G9pJw==}
engines: {node: '>=6.14.2'}
@@ -178,6 +478,11 @@ packages:
dev: false
optional: true
+ /clean-stack@2.2.0:
+ resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==}
+ engines: {node: '>=6'}
+ dev: false
+
/combined-stream@1.0.8:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'}
@@ -199,8 +504,12 @@ packages:
engines: {node: '>=0.4.0'}
dev: false
- /dfx@0.48.0:
- resolution: {integrity: sha512-mwLHSShglRtJAY/ZcA6GBCGuQoe4SKYXi1qPksUqO4EeYnzLbh/gx0i/YE7r7koSempk4UKTNf9kyvHixvi8+Q==}
+ /deprecation@2.3.1:
+ resolution: {integrity: sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==}
+ dev: false
+
+ /dfx@0.48.1:
+ resolution: {integrity: sha512-k1ZtD64kCSjzC/F4V+4TkmSzIuzk7BmZyjkeiKQaqUT1GQUKRTfZMuKosWMh27G26spiTGQQAWHKqeiI7JeFWw==}
dependencies:
'@effect-http/client': 0.27.0
'@effect/data': 0.12.5
@@ -230,6 +539,12 @@ packages:
resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==}
dev: true
+ /ecdsa-sig-formatter@1.0.11:
+ resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==}
+ dependencies:
+ safe-buffer: 5.2.1
+ dev: false
+
/effect-schema-class@0.4.0:
resolution: {integrity: sha512-LYO1AFK3+mXIrOi6iwHtjErKNj9LvM8Jcwj7fRs5nNEozbOgIWFbcy6voE95AznR4OVa8lUsnIHRUiOp0KHR4A==}
dependencies:
@@ -280,10 +595,30 @@ packages:
resolution: {integrity: sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==}
dev: true
+ /fromentries@1.3.2:
+ resolution: {integrity: sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg==}
+ dev: false
+
+ /gpt-tokenizer@2.1.1:
+ resolution: {integrity: sha512-WlX+vj6aPaZ71U6Bf18fem+5k58zlgh2a4nbc7KHy6aGVIyq3nCh709b/8momu34sV/5t/SpzWi8LayWD9uyDw==}
+ dependencies:
+ rfc4648: 1.5.2
+ dev: false
+
/html-entities@2.3.5:
resolution: {integrity: sha512-72TJlcMkYsEJASa/3HnX7VT59htM7iSHbH59NSZbtc+22Ap0Txnlx91sfeB+/A7wNZg7UxtZdhAW4y+/jimrdg==}
dev: false
+ /indent-string@4.0.0:
+ resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==}
+ engines: {node: '>=8'}
+ dev: false
+
+ /is-plain-object@5.0.0:
+ resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==}
+ engines: {node: '>=0.10.0'}
+ dev: false
+
/isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
dev: true
@@ -298,6 +633,47 @@ packages:
dev: false
optional: true
+ /jsonwebtoken@9.0.0:
+ resolution: {integrity: sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==}
+ engines: {node: '>=12', npm: '>=6'}
+ dependencies:
+ jws: 3.2.2
+ lodash: 4.17.21
+ ms: 2.1.3
+ semver: 7.5.1
+ dev: false
+
+ /jwa@1.4.1:
+ resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==}
+ dependencies:
+ buffer-equal-constant-time: 1.0.1
+ ecdsa-sig-formatter: 1.0.11
+ safe-buffer: 5.2.1
+ dev: false
+
+ /jws@3.2.2:
+ resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==}
+ dependencies:
+ jwa: 1.4.1
+ safe-buffer: 5.2.1
+ dev: false
+
+ /lodash@4.17.21:
+ resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
+ dev: false
+
+ /lru-cache@6.0.0:
+ resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
+ engines: {node: '>=10'}
+ dependencies:
+ yallist: 4.0.0
+ dev: false
+
+ /lru-cache@9.1.2:
+ resolution: {integrity: sha512-ERJq3FOzJTxBbFjZ7iDs+NiK4VI9Wz+RdrrAB8dio1oV+YvdPzUEE4QNiT2VD51DkIbCYRUUzCRkssXCHqSnKQ==}
+ engines: {node: 14 || >=16.14}
+ dev: false
+
/map-stream@0.1.0:
resolution: {integrity: sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==}
dev: true
@@ -314,16 +690,54 @@ packages:
mime-db: 1.52.0
dev: false
+ /ms@2.1.3:
+ resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
+ dev: false
+
/node-cleanup@2.1.2:
resolution: {integrity: sha512-qN8v/s2PAJwGUtr1/hYTpNKlD6Y9rc4p8KSmJXyGdYGZsDGKXrGThikLFP9OCHFeLeEpQzPwiAtdIvBLqm//Hw==}
dev: true
+ /node-fetch@2.6.11:
+ resolution: {integrity: sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w==}
+ engines: {node: 4.x || >=6.0.0}
+ peerDependencies:
+ encoding: ^0.1.0
+ peerDependenciesMeta:
+ encoding:
+ optional: true
+ dependencies:
+ whatwg-url: 5.0.0
+ dev: false
+
/node-gyp-build@4.6.0:
resolution: {integrity: sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==}
hasBin: true
dev: false
optional: true
+ /octokit@2.0.19:
+ resolution: {integrity: sha512-hSloK4MK78QGbAuBrtIir0bsxMoRVZE5CkwKSbSRH9lqv2hx9EwhCxtPqEF+BtHqLXkXdfUaGkJMyMBotYno+A==}
+ engines: {node: '>= 14'}
+ dependencies:
+ '@octokit/app': 13.1.5
+ '@octokit/core': 4.2.1
+ '@octokit/oauth-app': 4.2.2
+ '@octokit/plugin-paginate-rest': 6.1.2(@octokit/core@4.2.1)
+ '@octokit/plugin-rest-endpoint-methods': 7.2.0(@octokit/core@4.2.1)
+ '@octokit/plugin-retry': 4.1.6(@octokit/core@4.2.1)
+ '@octokit/plugin-throttling': 5.2.3(@octokit/core@4.2.1)
+ '@octokit/types': 9.3.0
+ transitivePeerDependencies:
+ - encoding
+ dev: false
+
+ /once@1.4.0:
+ resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
+ dependencies:
+ wrappy: 1.0.2
+ dev: false
+
/openai@3.2.1:
resolution: {integrity: sha512-762C9BNlJPbjjlWZi4WYK9iM2tAVAv0uUp1UmI34vb0CN5T2mjB/qM6RYBmNKMh/dN9fC+bxqPwWJZUTWW052A==}
dependencies:
@@ -362,6 +776,22 @@ packages:
resolution: {integrity: sha512-6Yg0ekpKICSjPswYOuC5sku/TSWaRYlA0qsXqJgM/d/4pLPHPuTxK7Nbf7jFKzAeedUhR8C7K9Uv63FBsSo8xQ==}
dev: false
+ /rfc4648@1.5.2:
+ resolution: {integrity: sha512-tLOizhR6YGovrEBLatX1sdcuhoSCXddw3mqNVAcKxGJ+J0hFeJ+SjeWCv5UPA/WU3YzWPPuCVYgXBKZUPGpKtg==}
+ dev: false
+
+ /safe-buffer@5.2.1:
+ resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
+ dev: false
+
+ /semver@7.5.1:
+ resolution: {integrity: sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==}
+ engines: {node: '>=10'}
+ hasBin: true
+ dependencies:
+ lru-cache: 6.0.0
+ dev: false
+
/shebang-command@2.0.0:
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
engines: {node: '>=8'}
@@ -395,6 +825,10 @@ packages:
resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
dev: true
+ /tr46@0.0.3:
+ resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
+ dev: false
+
/tsc-watch@6.0.4(typescript@5.1.3):
resolution: {integrity: sha512-cHvbvhjO86w2aGlaHgSCeQRl+Aqw6X6XN4sQMPZKF88GoP30O+oTuh5lRIJr5pgFWrRpF1AgXnJJ2DoFEIPHyg==}
engines: {node: '>=12.12.0'}
@@ -415,6 +849,17 @@ packages:
hasBin: true
dev: true
+ /universal-github-app-jwt@1.1.1:
+ resolution: {integrity: sha512-G33RTLrIBMFmlDV4u4CBF7dh71eWwykck4XgaxaIVeZKOYZRAAxvcGMRFTUclVY6xoUPQvO4Ne5wKGxYm/Yy9w==}
+ dependencies:
+ '@types/jsonwebtoken': 9.0.2
+ jsonwebtoken: 9.0.0
+ dev: false
+
+ /universal-user-agent@6.0.0:
+ resolution: {integrity: sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==}
+ dev: false
+
/utf-8-validate@6.0.3:
resolution: {integrity: sha512-uIuGf9TWQ/y+0Lp+KGZCMuJWc3N9BHA+l/UmHd/oUHwJJDeysyTRxNQVkbzsIWfGFbRe3OcgML/i0mvVRPOyDA==}
engines: {node: '>=6.14.2'}
@@ -424,6 +869,17 @@ packages:
dev: false
optional: true
+ /webidl-conversions@3.0.1:
+ resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
+ dev: false
+
+ /whatwg-url@5.0.0:
+ resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
+ dependencies:
+ tr46: 0.0.3
+ webidl-conversions: 3.0.1
+ dev: false
+
/which@2.0.2:
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
engines: {node: '>= 8'}
@@ -432,6 +888,10 @@ packages:
isexe: 2.0.0
dev: true
+ /wrappy@1.0.2:
+ resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
+ dev: false
+
/ws@8.13.0(bufferutil@4.0.7)(utf-8-validate@6.0.3):
resolution: {integrity: sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==}
engines: {node: '>=10.0.0'}
@@ -449,3 +909,7 @@ packages:
utf-8-validate: 6.0.3
dev: false
optional: true
+
+ /yallist@4.0.0:
+ resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
+ dev: false
diff --git a/src/Github.ts b/src/Github.ts
new file mode 100644
index 0000000..2111eb3
--- /dev/null
+++ b/src/Github.ts
@@ -0,0 +1,77 @@
+import type { OctokitResponse } from "@octokit/types"
+import {
+ Chunk,
+ Config,
+ ConfigSecret,
+ Effect,
+ Layer,
+ Option,
+ Stream,
+ Tag,
+ pipe,
+} from "bot/_common"
+import { Octokit } from "octokit"
+
+export interface GithubConfig {
+ readonly token: ConfigSecret.ConfigSecret
+}
+
+export class GithubError {
+ readonly _tag = "GithubError"
+ constructor(readonly reason: unknown) {}
+}
+
+const make = ({ token }: GithubConfig) => {
+ const octokit = new Octokit({ auth: ConfigSecret.value(token) })
+
+ const rest = octokit.rest
+ type Endpoints = typeof rest
+
+ const request = (f: (_: Endpoints) => Promise) =>
+ Effect.tryCatchPromise(
+ () => f(rest),
+ reason => new GithubError(reason),
+ )
+
+ const wrap =
+ (
+ f: (_: Endpoints) => (...args: Args) => Promise>,
+ ) =>
+ (...args: Args) =>
+ Effect.map(
+ Effect.tryCatchPromise(
+ () => f(rest)(...args),
+ reason => new GithubError(reason),
+ ),
+ _ => _.data,
+ )
+
+ const stream = (
+ f: (_: Endpoints, page: number) => Promise>,
+ ) =>
+ Stream.paginateChunkEffect(0, page =>
+ Effect.map(
+ Effect.tryCatchPromise(
+ () => f(rest, page),
+ reason => new GithubError(reason),
+ ),
+ _ => [Chunk.fromIterable(_.data), maybeNextPage(page, _.headers.link)],
+ ),
+ )
+
+ return { octokit, token, request, wrap, stream }
+}
+
+export interface Github extends ReturnType {}
+export const Github = Tag()
+export const makeLayer = (_: Config.Config.Wrap) =>
+ Layer.effect(Github, Effect.map(Effect.config(Config.unwrap(_)), make))
+
+// == helpers
+
+const maybeNextPage = (page: number, linkHeader?: string) =>
+ pipe(
+ Option.fromNullable(linkHeader),
+ Option.filter(_ => _.includes(`rel=\"next\"`)),
+ Option.as(page + 1),
+ )
diff --git a/src/Issueifier.ts b/src/Issueifier.ts
new file mode 100644
index 0000000..6c7cbcc
--- /dev/null
+++ b/src/Issueifier.ts
@@ -0,0 +1,152 @@
+import { ChannelsCache, ChannelsCacheLive } from "bot/ChannelsCache"
+import { Github } from "bot/Github"
+import { Messages, MessagesLive } from "bot/Messages"
+import { OpenAI, OpenAIMessage } from "bot/OpenAI"
+import { Summarizer, SummarizerLive } from "bot/Summarizer"
+import { Chunk, Config, Data, Effect, Layer, Stream, pipe } from "bot/_common"
+import { Discord, DiscordREST, Ix } from "dfx"
+import { InteractionsRegistry, InteractionsRegistryLive } from "dfx/gateway"
+
+export interface IssueifierConfig {
+ readonly githubRepo: string
+}
+
+export class NotInThreadError extends Data.TaggedClass(
+ "NotInThreadError",
+)<{}> {}
+
+const make = ({ githubRepo }: IssueifierConfig) =>
+ Effect.gen(function* (_) {
+ const rest = yield* _(DiscordREST)
+ const channels = yield* _(ChannelsCache)
+ const openai = yield* _(OpenAI)
+ const messages = yield* _(Messages)
+ const registry = yield* _(InteractionsRegistry)
+ const scope = yield* _(Effect.scope())
+ const github = yield* _(Github)
+ const summarizer = yield* _(Summarizer)
+
+ const [repoOwner, repoName] = githubRepo.split("/")
+ const createGithubIssue = github.wrap(_ => _.issues.create)
+
+ const application = yield* _(
+ Effect.flatMap(rest.getCurrentBotApplicationInformation(), _ => _.json),
+ )
+
+ const createIssue = (channel: Discord.Channel) =>
+ pipe(
+ messages.cleanForChannel(channel),
+ Stream.runCollect,
+ Effect.map(Chunk.reverse),
+ Effect.bindTo("messages"),
+ Effect.let("openAiMessages", ({ messages }) =>
+ Chunk.map(
+ messages,
+ (msg): OpenAIMessage => ({
+ bot: false,
+ name: msg.author.username,
+ content: msg.content,
+ }),
+ ),
+ ),
+ Effect.flatMap(({ openAiMessages, messages }) =>
+ Effect.all({
+ summary: openai.generateSummary(
+ channel.name!,
+ Chunk.toReadonlyArray(openAiMessages),
+ ),
+ fullThread: summarizer.messages(channel, messages),
+ }),
+ ),
+ Effect.flatMap(({ summary, fullThread }) =>
+ createGithubIssue({
+ owner: repoOwner,
+ repo: repoName,
+ title: `From Discord: ${channel.name}`,
+ body: `${summary}
+
+## Discord thread
+
+
+Click to expand
+
+${fullThread}
+
+ `,
+ }),
+ ),
+ )
+
+ const followUp = (context: Discord.Interaction, channel: Discord.Channel) =>
+ pipe(
+ createIssue(channel),
+ Effect.tap(issue =>
+ rest.editOriginalInteractionResponse(application.id, context.token, {
+ content: `Created Github issue for thread: ${issue.html_url}`,
+ }),
+ ),
+ Effect.tapErrorCause(() =>
+ rest.deleteOriginalInteractionResponse(application.id, context.token),
+ ),
+ Effect.catchAllCause(Effect.logErrorCause),
+ )
+
+ const command = Ix.global(
+ {
+ name: "issueify",
+ description:
+ "Convert this thread into an issue for the Effect Website repo",
+ },
+ pipe(
+ Effect.all({ context: Ix.Interaction }),
+ Effect.bind("channel", ({ context }) =>
+ channels.get(context.guild_id!, context.channel_id!),
+ ),
+ Effect.filterOrFail(
+ ({ channel }) => channel.type === Discord.ChannelType.PUBLIC_THREAD,
+ () => new NotInThreadError(),
+ ),
+ Effect.tap(({ context, channel }) =>
+ Effect.forkIn(followUp(context, channel), scope),
+ ),
+ Effect.as(
+ Ix.response({
+ type: Discord.InteractionCallbackType.CHANNEL_MESSAGE_WITH_SOURCE,
+ data: {
+ content: "Creating issue on Github...",
+ },
+ }),
+ ),
+ ),
+ )
+
+ const ix = Ix.builder
+ .add(command)
+ .catchTagRespond("NotInThreadError", () =>
+ Effect.succeed(
+ Ix.response({
+ type: Discord.InteractionCallbackType.CHANNEL_MESSAGE_WITH_SOURCE,
+ data: {
+ content: "This command can only be used in a thread",
+ flags: Discord.MessageFlag.EPHEMERAL,
+ },
+ }),
+ ),
+ )
+ .catchAllCause(Effect.logErrorCause)
+
+ yield* _(registry.register(ix))
+ })
+
+export const makeLayer = (config: Config.Config.Wrap) =>
+ Layer.provide(
+ Layer.mergeAll(
+ ChannelsCacheLive,
+ InteractionsRegistryLive,
+ MessagesLive,
+ SummarizerLive,
+ ),
+ Layer.scopedDiscard(
+ Effect.flatMap(Effect.config(Config.unwrap(config)), make),
+ ),
+ )
diff --git a/src/Mentions.ts b/src/Mentions.ts
index 2d96b75..ba206a2 100644
--- a/src/Mentions.ts
+++ b/src/Mentions.ts
@@ -49,11 +49,11 @@ const make = Effect.gen(function* (_) {
.filter(msg => msg.content.trim().length > 0)
.map(
(msg): OpenAIMessage => ({
- content:
+ content: msg.content,
+ name:
msg.author.id === botUser.id
- ? msg.content
- : `<@${msg.author.id}> said:
-${msg.content}`,
+ ? undefined
+ : `<@${msg.author.id}>`,
bot: msg.author.id === botUser.id,
}),
),
diff --git a/src/Messages.ts b/src/Messages.ts
new file mode 100644
index 0000000..2c7f099
--- /dev/null
+++ b/src/Messages.ts
@@ -0,0 +1,114 @@
+import { MemberCache, MemberCacheLive } from "bot/MemberCache"
+import { Chunk, Effect, Layer, Option, Stream, Tag, pipe } from "bot/_common"
+import { Discord, DiscordREST } from "dfx"
+
+export const cleanupMarkdown = (content: string) =>
+ content
+ .replace(/```ts\b/g, "```typescript")
+ .replace(/^```/, "\n```")
+ .replace(/[^\n]```/gm, "\n\n```")
+ .replace(/([^\n])\n```([^\n]*\n[^\n])/gm, "$1\n\n```$2")
+
+const make = Effect.gen(function* (_) {
+ const rest = yield* _(DiscordREST)
+ const members = yield* _(MemberCache)
+
+ const replaceMentions = (guildId: Discord.Snowflake, content: string) =>
+ Effect.gen(function* (_) {
+ const mentions = yield* _(
+ Effect.forEachPar(content.matchAll(/<@(\d+)>/g), ([, userId]) =>
+ Effect.option(members.get(guildId, userId as Discord.Snowflake)),
+ ),
+ )
+
+ return mentions.reduce(
+ (content, member) =>
+ Option.match(
+ member,
+ () => content,
+ member =>
+ content.replace(
+ new RegExp(`<@${member.user!.id}>`, "g"),
+ `**@${member.nick ?? member.user!.username}**`,
+ ),
+ ),
+ content,
+ )
+ })
+
+ const regularForChannel = (channelId: string) =>
+ pipe(
+ Stream.paginateChunkEffect(Option.none(), before =>
+ pipe(
+ rest.getChannelMessages(channelId, {
+ limit: 100,
+ before: Option.getOrUndefined(before),
+ }),
+ Effect.flatMap(_ => _.json),
+ Effect.map(messages =>
+ messages.length < 100
+ ? ([
+ Chunk.unsafeFromArray(messages),
+ Option.none>(),
+ ] as const)
+ : ([
+ Chunk.unsafeFromArray(messages),
+ Option.some(Option.some(messages[messages.length - 1].id)),
+ ] as const),
+ ),
+ ),
+ ),
+
+ // only include normal messages
+ Stream.flatMapPar(Number.MAX_SAFE_INTEGER, msg => {
+ if (msg.type === Discord.MessageType.THREAD_STARTER_MESSAGE) {
+ return Effect.flatMap(
+ rest.getChannelMessage(
+ msg.message_reference!.channel_id!,
+ msg.message_reference!.message_id!,
+ ),
+ _ => _.json,
+ )
+ } else if (
+ msg.content !== "" &&
+ (msg.type === Discord.MessageType.REPLY ||
+ msg.type === Discord.MessageType.DEFAULT)
+ ) {
+ return Stream.succeed(msg)
+ }
+
+ return Stream.empty
+ }),
+ )
+
+ const cleanForChannel = (channel: Discord.Channel) =>
+ pipe(
+ regularForChannel(channel.id),
+ Stream.map(msg => ({
+ ...msg,
+ content: cleanupMarkdown(msg.content),
+ })),
+ Stream.flatMapPar(Number.MAX_SAFE_INTEGER, msg =>
+ Effect.map(
+ replaceMentions(channel.guild_id!, msg.content),
+ (content): Discord.Message => ({
+ ...msg,
+ content,
+ }),
+ ),
+ ),
+ )
+
+ return {
+ regularForChannel,
+ cleanForChannel,
+ replaceMentions,
+ } as const
+})
+
+export interface Messages extends Effect.Effect.Success {}
+export const Messages = Tag()
+export const MessagesLive = Layer.provide(
+ Layer.mergeAll(MemberCacheLive),
+ Layer.effect(Messages, make),
+)
diff --git a/src/OpenAI.ts b/src/OpenAI.ts
index 732c70a..3915b37 100644
--- a/src/OpenAI.ts
+++ b/src/OpenAI.ts
@@ -11,6 +11,7 @@ import {
} from "bot/_common"
import * as Str from "bot/utils/String"
import { ChatCompletionRequestMessage, Configuration, OpenAIApi } from "openai"
+import * as Tokenizer from "gpt-tokenizer"
export interface OpenAIOptions {
readonly apiKey: ConfigSecret.ConfigSecret
@@ -23,6 +24,7 @@ export class OpenAIError extends Data.TaggedClass("OpenAIError")<{
export interface OpenAIMessage {
readonly bot: boolean
+ readonly name?: string
readonly content: string
}
@@ -113,7 +115,50 @@ The title of this conversation is "${title}".`,
_ => Option.fromNullable(_.data.choices[0]?.message?.content),
)
- return { client, call, generateTitle, generateReply } as const
+ const generateSummary = (
+ title: string,
+ messages: ReadonlyArray,
+ ) =>
+ Effect.flatMap(
+ call((_, signal) =>
+ _.createChatCompletion(
+ {
+ model: "gpt-3.5-turbo",
+ temperature: 0.25,
+ messages: [
+ {
+ role: "system",
+ content: `You are a helpful assistant for the Effect-TS typescript library.
+
+The title of this chat is "${title}".`,
+ },
+ ...limitMessageTokens(messages, 3300).map(
+ ({ content, bot, name }): ChatCompletionRequestMessage => ({
+ role: bot ? "assistant" : "user",
+ name,
+ content,
+ }),
+ ),
+ {
+ role: "user",
+ content:
+ "Summarize the conversation, then add some key takeaways.",
+ },
+ ],
+ },
+ { signal },
+ ),
+ ),
+ _ => Option.fromNullable(_.data.choices[0]?.message?.content),
+ )
+
+ return {
+ client,
+ call,
+ generateTitle,
+ generateReply,
+ generateSummary,
+ } as const
}
export interface OpenAI extends ReturnType {}
@@ -122,3 +167,20 @@ export const makeLayer = (config: Config.Config.Wrap) =>
Layer.effect(OpenAI, Effect.map(Effect.config(Config.unwrap(config)), make))
const cleanTitle = flow(Str.firstParagraph, Str.removeQuotes, Str.removePeriod)
+
+const limitMessageTokens = (
+ messages: ReadonlyArray,
+ count: number,
+): ReadonlyArray => {
+ let content = ""
+ const newMessages: OpenAIMessage[] = []
+ for (const message of messages) {
+ content += message.content
+ const tokens = Tokenizer.encode(content).length
+ if (tokens > count) {
+ break
+ }
+ newMessages.push(message)
+ }
+ return newMessages
+}
diff --git a/src/Summarizer.ts b/src/Summarizer.ts
index 03f9acb..ca08864 100644
--- a/src/Summarizer.ts
+++ b/src/Summarizer.ts
@@ -9,10 +9,12 @@ import {
Layer,
Option,
Stream,
+ Tag,
pipe,
} from "bot/_common"
import { Discord, DiscordREST, Ix } from "dfx"
import { InteractionsRegistry, InteractionsRegistryLive } from "dfx/gateway"
+import { Messages, MessagesLive } from "bot/Messages"
export class NotInThreadError extends Data.TaggedClass(
"NotInThreadError",
@@ -28,54 +30,36 @@ const make = Effect.gen(function* (_) {
const channels = yield* _(ChannelsCache)
const registry = yield* _(InteractionsRegistry)
const members = yield* _(MemberCache)
+ const messages = yield* _(Messages)
const scope = yield* _(Effect.scope())
const application = yield* _(
Effect.flatMap(rest.getCurrentBotApplicationInformation(), _ => _.json),
)
- const getAllMessages = (channelId: string) =>
+ const summarizeThread = (channel: Discord.Channel, small = true) =>
pipe(
- Stream.paginateChunkEffect(Option.none(), before =>
- pipe(
- rest.getChannelMessages(channelId, {
- limit: 100,
- before: Option.getOrUndefined(before),
- }),
- Effect.flatMap(_ => _.json),
- Effect.map(messages =>
- messages.length < 100
- ? ([
- Chunk.unsafeFromArray(messages),
- Option.none>(),
- ] as const)
- : ([
- Chunk.unsafeFromArray(messages),
- Option.some(Option.some(messages[messages.length - 1].id)),
- ] as const),
- ),
+ Effect.all({
+ parentChannel: channels.get(channel.guild_id!, channel.parent_id!),
+ messages: Effect.map(
+ Stream.runCollect(messages.cleanForChannel(channel)),
+ Chunk.reverse,
),
+ }),
+ Effect.flatMap(({ parentChannel, messages }) =>
+ summarize(parentChannel, channel, messages, small),
),
+ )
- // only include normal messages
- Stream.flatMapPar(Number.MAX_SAFE_INTEGER, msg => {
- if (msg.type === Discord.MessageType.THREAD_STARTER_MESSAGE) {
- return Effect.flatMap(
- rest.getChannelMessage(
- msg.message_reference!.channel_id!,
- msg.message_reference!.message_id!,
- ),
- _ => _.json,
- )
- } else if (
- msg.content !== "" &&
- (msg.type === Discord.MessageType.REPLY ||
- msg.type === Discord.MessageType.DEFAULT)
- ) {
- return Stream.succeed(msg)
- }
-
- return Stream.empty
- }),
+ const summarizeWithMessages = (
+ channel: Discord.Channel,
+ messages: Chunk.Chunk,
+ small = true,
+ ) =>
+ pipe(
+ channels.get(channel.guild_id!, channel.parent_id!),
+ Effect.flatMap(parentChannel =>
+ summarize(parentChannel, channel, messages, small),
+ ),
)
const summarize = (
@@ -133,34 +117,8 @@ ${messageContent.join("\n\n")}`,
message.timestamp,
).toUTCString()}${smallClose}${smallClose}`
- const content = `${header}
-${message.content
- .replace(/```ts\b/g, "```typescript")
- .replace(/^```/, "\n```")
- .replace(/[^\n]```/gm, "\n\n```")
- .replace(/([^\n])\n```([^\n]*\n[^\n])/gm, "$1\n\n```$2")}`
-
- const mentions = yield* _(
- Effect.forEachPar(content.matchAll(/<@(\d+)>/g), ([, userId]) =>
- Effect.option(
- members.get(thread.guild_id!, userId as Discord.Snowflake),
- ),
- ),
- )
-
- return mentions.reduce(
- (content, member) =>
- Option.match(
- member,
- () => content,
- member =>
- content.replace(
- new RegExp(`<@${member.user!.id}>`, "g"),
- `**@${member.nick ?? member.user!.username}**`,
- ),
- ),
- content,
- )
+ return `${header}
+${message.content}`
})
const followUpResponse = (
@@ -169,19 +127,8 @@ ${message.content
small: boolean,
) =>
pipe(
- Effect.all({
- parentChannel: channels.get(channel.guild_id!, channel.parent_id!),
- }),
- Effect.bind("messages", () =>
- Effect.map(
- Stream.runCollect(getAllMessages(channel.id)),
- Chunk.reverse,
- ),
- ),
- Effect.bind("summary", ({ parentChannel, messages }) =>
- summarize(parentChannel, channel, messages, small),
- ),
- Effect.tap(({ summary }) => {
+ summarizeThread(channel, small),
+ Effect.tap(summary => {
const formData = new FormData()
formData.append(
@@ -264,9 +211,22 @@ ${message.content
.catchAllCause(Effect.logErrorCause)
yield* _(registry.register(ix))
+
+ return {
+ thread: summarizeThread,
+ messages: summarizeWithMessages,
+ message: summarizeMessage,
+ } as const
})
+export interface Summarizer extends Effect.Effect.Success {}
+export const Summarizer = Tag()
export const SummarizerLive = Layer.provide(
- Layer.mergeAll(ChannelsCacheLive, InteractionsRegistryLive, MemberCacheLive),
- Layer.scopedDiscard(make),
+ Layer.mergeAll(
+ ChannelsCacheLive,
+ InteractionsRegistryLive,
+ MemberCacheLive,
+ MessagesLive,
+ ),
+ Layer.scoped(Summarizer, make),
)
diff --git a/src/main.ts b/src/main.ts
index d611204..22b9a3c 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,6 +1,8 @@
import * as AutoThreads from "bot/AutoThreads"
import { BotLive } from "bot/Bot"
import { DocsLookupLive } from "bot/DocsLookup"
+import * as Github from "bot/Github"
+import * as Issueifier from "bot/Issueifier"
import { MentionsLive } from "bot/Mentions"
import * as NoEmbed from "bot/NoEmbed"
import * as OpenAI from "bot/OpenAI"
@@ -41,12 +43,24 @@ const NoEmbedLive = NoEmbed.makeLayer({
),
})
+const IssueifierLive = Issueifier.makeLayer({
+ githubRepo: Config.withDefault(
+ Config.string("ISSUEIFIER_REPO"),
+ "effect-ts/website",
+ ),
+})
+
+const GithubLive = Github.makeLayer({
+ token: Config.secret("GITHUB_TOKEN"),
+})
+
const MainLive = pipe(
- Layer.mergeAll(DiscordLive, OpenAILive),
+ Layer.mergeAll(DiscordLive, GithubLive, OpenAILive),
Layer.provide(
Layer.mergeAll(
AutoThreadsLive,
DocsLookupLive,
+ IssueifierLive,
NoEmbedLive,
MentionsLive,
SummarizerLive,
diff --git a/tsconfig.json b/tsconfig.json
index 9972ec8..ee8e119 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -8,6 +8,7 @@
"moduleResolution": "NodeNext",
"module": "ESNext",
"allowSyntheticDefaultImports": true,
+ "noErrorTruncation": true,
"lib": ["ESNext", "DOM"],
"sourceMap": true,
"strict": true,