├── public ├── favicon.ico ├── admin │ ├── index.html │ └── script.js ├── index.html └── styles.css ├── .prettierrc ├── .dev.vars.example ├── .editorconfig ├── test ├── tsconfig.json └── index.spec.ts ├── vitest.config.mts ├── agenda.md ├── worker-configuration.d.ts ├── package.json ├── README.md ├── src ├── before.ts └── index.ts ├── .gitignore ├── wrangler.toml └── tsconfig.json /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-shorty-dot-dev/main/public/favicon.ico -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 140, 3 | "singleQuote": true, 4 | "semi": true, 5 | "useTabs": true 6 | } 7 | -------------------------------------------------------------------------------- /.dev.vars.example: -------------------------------------------------------------------------------- 1 | CLOUDFLARE_ACCOUNT_ID="YOUR-CF-ACCOUNT-ID" 2 | CLOUDFLARE_API_TOKEN="A-WORKERS-ANALYTICS-ENGINE-READ-TOKEN" 3 | JWT_SECRET="A-SECRET-TO-HIDE-FOR-API" 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = tab 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.yml] 12 | indent_style = space 13 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "types": ["@cloudflare/workers-types/experimental", "@cloudflare/vitest-pool-workers"] 5 | }, 6 | "include": ["./**/*.ts", "../src/env.d.ts"], 7 | "exclude": [] 8 | } 9 | -------------------------------------------------------------------------------- /vitest.config.mts: -------------------------------------------------------------------------------- 1 | import { defineWorkersConfig } from '@cloudflare/vitest-pool-workers/config'; 2 | 3 | export default defineWorkersConfig({ 4 | test: { 5 | poolOptions: { 6 | workers: { 7 | wrangler: { configPath: './wrangler.toml' }, 8 | }, 9 | }, 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /agenda.md: -------------------------------------------------------------------------------- 1 | # shrty.dev 2 | 3 | - [x] 🩳 Actual need 4 | - [x] 👎 bit.ly 5 | - [x] https://shrty.dev/hack-ai 6 | - [x] 👷‍♀️ #BuildInPublic === gsd? 7 | - [x] 🖍️ I am no good at UI/UX 8 | - [x] 🤔 Additional side needs...gssd 9 | - [x] Embedded function calling demo 10 | - [x] More to add/explore 11 | - [x] 🪟 Exposed some rough edges 12 | 13 | https://shrty.dev/repo 14 | -------------------------------------------------------------------------------- /worker-configuration.d.ts: -------------------------------------------------------------------------------- 1 | // Generated by Wrangler on Tue Jul 09 2024 10:27:20 GMT-0700 (Pacific Daylight Time) 2 | // by running `wrangler types --env-interface CloudflareBindings` 3 | 4 | interface CloudflareBindings { 5 | URLS: KVNamespace; 6 | CLOUDFLARE_ACCOUNT_ID: string; 7 | CLOUDFLARE_API_TOKEN: string; 8 | JWT_SECRET: string; 9 | TRACKER: AnalyticsEngineDataset; 10 | AI: Ai; 11 | } 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shrty-dot-dev", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "deploy": "wrangler deploy", 7 | "dev": "wrangler dev", 8 | "start": "wrangler dev", 9 | "test": "vitest", 10 | "cf-typegen": "wrangler types --env-interface CloudflareBindings" 11 | }, 12 | "devDependencies": { 13 | "@cloudflare/vitest-pool-workers": "^0.4.5", 14 | "@cloudflare/workers-types": "^4.20240620.0", 15 | "@types/common-tags": "^1.8.4", 16 | "typescript": "^5.4.5", 17 | "vitest": "1.5.0", 18 | "wrangler": "^3.78.12" 19 | }, 20 | "dependencies": { 21 | "@cloudflare/ai-utils": "^1.0.1", 22 | "cloudflare": "^3.4.0", 23 | "common-tags": "^1.8.2", 24 | "fetch-event-source": "^1.0.0-alpha.2", 25 | "fetch-event-stream": "^0.1.5", 26 | "hono": "^4.4.8" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /public/admin/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Shorty Chat 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |

shrty Chat

15 |
16 | 17 |
18 |
19 |
20 |
21 | 22 | 23 |
24 |
25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /test/index.spec.ts: -------------------------------------------------------------------------------- 1 | // test/index.spec.ts 2 | import { env, createExecutionContext, waitOnExecutionContext, SELF } from 'cloudflare:test'; 3 | import { describe, it, expect } from 'vitest'; 4 | import worker from '../src/index'; 5 | 6 | // For now, you'll need to do something like this to get a correctly-typed 7 | // `Request` to pass to `worker.fetch()`. 8 | const IncomingRequest = Request; 9 | 10 | describe('Hello World worker', () => { 11 | it('responds with Hello World! (unit style)', async () => { 12 | const request = new IncomingRequest('http://example.com'); 13 | // Create an empty context to pass to `worker.fetch()`. 14 | const ctx = createExecutionContext(); 15 | const response = await worker.fetch(request, env, ctx); 16 | // Wait for all `Promise`s passed to `ctx.waitUntil()` to settle before running test assertions 17 | await waitOnExecutionContext(ctx); 18 | expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`); 19 | }); 20 | 21 | it('responds with Hello World! (integration style)', async () => { 22 | const response = await SELF.fetch('https://example.com'); 23 | expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # shrty.dev 2 | 3 | This is a #BuildInPublic project of a URL Shortening service built on the [Cloudflare Developer Platform](https://developers.cloudflare.com). 4 | 5 | It makes use of the Key Value service [KV](https://developers.cloudflare.com/kv) to store the shorty and the URL. 6 | 7 | It also uses the [Workers Analytics Engine](https://developers.cloudflare.com/analytics/analytics-engine/) to track and report on usage. 8 | 9 | ## Resources 10 | 11 | [![Watch shrty.dev Admin IA on YouTube ](https://img.youtube.com/vi/MlV9Kvkh9hw/0.jpg)](https://youtu.be/MlV9Kvkh9hw) 12 | 13 | ## Setup your own 14 | 15 | ### Setup 16 | 17 | Build a new KV service for yourself to track the URLs 18 | 19 | ```bash 20 | npx wrangler kv:namespace create URLS 21 | ``` 22 | 23 | Replace wrangler.toml settings for the KV section 24 | 25 | Create a new [Workers Analytics Engine API token](https://developers.cloudflare.com/analytics/analytics-engine/sql-api/) 26 | 27 | Copy the [.dev.vars.example](./.dev.vars.example) to `.dev.vars` (for local development) 28 | 29 | Regenerate types 30 | 31 | ```bash 32 | npx wrangler cf-typegen 33 | ``` 34 | 35 | ## Develop 36 | 37 | ```bash 38 | npm run dev 39 | ``` 40 | 41 | ## Deploy 42 | 43 | ```bash 44 | npm run deploy 45 | ``` 46 | 47 | 48 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Shorty 7 | 8 | 9 | 45 | 46 | 47 |
48 |
49 |

shrty

50 |

partying as if it were your birthday

51 |
52 |
53 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /src/before.ts: -------------------------------------------------------------------------------- 1 | app.post('/chat', async (c) => { 2 | const payload = await c.req.json(); 3 | const messages = payload.messages || []; 4 | //console.log({ submittedMessages: messages }); 5 | messages.unshift({ 6 | role: 'system', 7 | content: SHORTY_SYSTEM_MESSAGE, 8 | }); 9 | let result: AiTextGenerationOutput = await c.env.AI.run('@hf/nousresearch/hermes-2-pro-mistral-7b', { 10 | messages, 11 | tools, 12 | }); 13 | while (result.tool_calls !== undefined) { 14 | for (const tool_call of result.tool_calls) { 15 | console.log('Tool Call', JSON.stringify(tool_call)); 16 | let fnResponse; 17 | switch (tool_call.name) { 18 | case 'createShorty': 19 | const override = tool_call.parameters?.override || false; 20 | fnResponse = await addUrl(c.env, tool_call.arguments.slug, tool_call.arguments.url, override); 21 | break; 22 | case 'getClicksByCountryReport': 23 | const slug = tool_call.arguments.slug; 24 | const sql = `SELECT 25 | blob4 as 'country', 26 | COUNT() as 'total' 27 | FROM 28 | link_clicks 29 | WHERE blob1='${slug}' 30 | GROUP BY country`; 31 | fnResponse = await queryClicks(c.env, sql); 32 | break; 33 | default: 34 | messages.push({ role: 'tool', name: tool_call.name, content: `ERROR: Tool not found "${tool_call.name}"` }); 35 | break; 36 | } 37 | if (fnResponse !== undefined) { 38 | messages.push({ role: 'tool', name: tool_call.name, content: JSON.stringify(fnResponse) }); 39 | result = await c.env.AI.run('@hf/nousresearch/hermes-2-pro-mistral-7b', { 40 | messages, 41 | tools, 42 | }); 43 | if (result.response !== null) { 44 | messages.push({ role: 'assistant', content: result.response }); 45 | } 46 | } 47 | } 48 | } 49 | const finalMessage = messages[messages.length - 1]; 50 | console.log({ finalMessage }); 51 | if (finalMessage.role !== 'assistant') { 52 | messages.push({ role: 'assistant', content: result.response }); 53 | } 54 | // Remove the system message 55 | messages.splice(0, 1); 56 | return c.json({ messages }); 57 | }); 58 | -------------------------------------------------------------------------------- /public/admin/script.js: -------------------------------------------------------------------------------- 1 | document.addEventListener('DOMContentLoaded', () => { 2 | const messageInput = document.getElementById('message-input'); 3 | const sendButton = document.getElementById('send-button'); 4 | const chatMessages = document.getElementById('chat-messages'); 5 | const clearButton = document.getElementById('clear-button'); 6 | 7 | function getMessages() { 8 | return JSON.parse(localStorage.getItem('messages')) || []; 9 | } 10 | 11 | function setMessages(messages) { 12 | localStorage.setItem('messages', JSON.stringify(messages)); 13 | return true; 14 | } 15 | 16 | const messages = getMessages(); 17 | // Load messages from LocalStorage 18 | messages.forEach(appendUiMessage); 19 | 20 | sendButton.addEventListener('click', sendMessage); 21 | messageInput.addEventListener('keypress', (e) => { 22 | if (e.key === 'Enter') { 23 | sendMessage(); 24 | } 25 | }); 26 | 27 | clearButton.addEventListener('click', clearMessages); 28 | 29 | async function sendMessage() { 30 | const messageText = messageInput.value.trim(); 31 | if (messageText) { 32 | const message = { 33 | role: 'user', 34 | content: messageText, 35 | }; 36 | const messages = getMessages(); 37 | messages.push(message); 38 | setMessages(messages); 39 | appendUiMessage(message); 40 | messageInput.value = ''; 41 | 42 | // Send message to server 43 | const response = await fetch('/admin/chat', { 44 | method: 'POST', 45 | headers: { 46 | 'Content-Type': 'application/json', 47 | }, 48 | body: JSON.stringify({ messages }), 49 | }); 50 | const assistantMsg = { role: 'assistant', content: '' }; 51 | // Create the placeholder to stream into 52 | const assistantResponse = appendUiMessage(assistantMsg); 53 | const reader = response.body.pipeThrough(new TextDecoderStream()).getReader(); 54 | while (true) { 55 | const { value, done } = await reader.read(); 56 | if (done) { 57 | console.log('Stream done'); 58 | // Add to the messages 59 | messages.push(assistantMsg); 60 | // And store them for later 61 | setMessages(messages); 62 | break; 63 | } 64 | assistantMsg.content += value; 65 | // Do not wipe out the model display 66 | assistantResponse.innerHTML = assistantMsg.content; 67 | } 68 | } 69 | } 70 | 71 | function appendUiMessage(message) { 72 | const messageElement = document.createElement('div'); 73 | messageElement.classList.add('message'); 74 | if (message.role === 'user') { 75 | messageElement.classList.add('user'); 76 | } else if (message.role === 'assistant') { 77 | messageElement.classList.add('assistant'); 78 | } 79 | messageElement.textContent = message.content; 80 | chatMessages.appendChild(messageElement); 81 | chatMessages.scrollTop = chatMessages.scrollHeight; 82 | return messageElement; 83 | } 84 | 85 | function clearMessages() { 86 | localStorage.removeItem('messages'); 87 | chatMessages.innerHTML = ''; 88 | } 89 | }); 90 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | 3 | logs 4 | _.log 5 | npm-debug.log_ 6 | yarn-debug.log* 7 | yarn-error.log* 8 | lerna-debug.log* 9 | .pnpm-debug.log* 10 | 11 | # Diagnostic reports (https://nodejs.org/api/report.html) 12 | 13 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 14 | 15 | # Runtime data 16 | 17 | pids 18 | _.pid 19 | _.seed 20 | \*.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | 28 | coverage 29 | \*.lcov 30 | 31 | # nyc test coverage 32 | 33 | .nyc_output 34 | 35 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 36 | 37 | .grunt 38 | 39 | # Bower dependency directory (https://bower.io/) 40 | 41 | bower_components 42 | 43 | # node-waf configuration 44 | 45 | .lock-wscript 46 | 47 | # Compiled binary addons (https://nodejs.org/api/addons.html) 48 | 49 | build/Release 50 | 51 | # Dependency directories 52 | 53 | node_modules/ 54 | jspm_packages/ 55 | 56 | # Snowpack dependency directory (https://snowpack.dev/) 57 | 58 | web_modules/ 59 | 60 | # TypeScript cache 61 | 62 | \*.tsbuildinfo 63 | 64 | # Optional npm cache directory 65 | 66 | .npm 67 | 68 | # Optional eslint cache 69 | 70 | .eslintcache 71 | 72 | # Optional stylelint cache 73 | 74 | .stylelintcache 75 | 76 | # Microbundle cache 77 | 78 | .rpt2_cache/ 79 | .rts2_cache_cjs/ 80 | .rts2_cache_es/ 81 | .rts2_cache_umd/ 82 | 83 | # Optional REPL history 84 | 85 | .node_repl_history 86 | 87 | # Output of 'npm pack' 88 | 89 | \*.tgz 90 | 91 | # Yarn Integrity file 92 | 93 | .yarn-integrity 94 | 95 | # dotenv environment variable files 96 | 97 | .env 98 | .env.development.local 99 | .env.test.local 100 | .env.production.local 101 | .env.local 102 | 103 | # parcel-bundler cache (https://parceljs.org/) 104 | 105 | .cache 106 | .parcel-cache 107 | 108 | # Next.js build output 109 | 110 | .next 111 | out 112 | 113 | # Nuxt.js build / generate output 114 | 115 | .nuxt 116 | dist 117 | 118 | # Gatsby files 119 | 120 | .cache/ 121 | 122 | # Comment in the public line in if your project uses Gatsby and not Next.js 123 | 124 | # https://nextjs.org/blog/next-9-1#public-directory-support 125 | 126 | # public 127 | 128 | # vuepress build output 129 | 130 | .vuepress/dist 131 | 132 | # vuepress v2.x temp and cache directory 133 | 134 | .temp 135 | .cache 136 | 137 | # Docusaurus cache and generated files 138 | 139 | .docusaurus 140 | 141 | # Serverless directories 142 | 143 | .serverless/ 144 | 145 | # FuseBox cache 146 | 147 | .fusebox/ 148 | 149 | # DynamoDB Local files 150 | 151 | .dynamodb/ 152 | 153 | # TernJS port file 154 | 155 | .tern-port 156 | 157 | # Stores VSCode versions used for testing VSCode extensions 158 | 159 | .vscode-test 160 | 161 | # yarn v2 162 | 163 | .yarn/cache 164 | .yarn/unplugged 165 | .yarn/build-state.yml 166 | .yarn/install-state.gz 167 | .pnp.\* 168 | 169 | # wrangler project 170 | 171 | .dev.vars 172 | .wrangler/ 173 | -------------------------------------------------------------------------------- /public/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: 'Press Start 2P', cursive; 4 | background-color: #f0f0f0; 5 | color: #000; 6 | image-rendering: pixelated; 7 | display: flex; 8 | flex-direction: column; 9 | justify-content: center; 10 | align-items: center; 11 | height: 100vh; 12 | } 13 | 14 | .chat-container { 15 | display: flex; 16 | flex-direction: column; 17 | height: 90vh; 18 | width: 90vw; 19 | justify-content: space-between; 20 | border: 4px solid #000; 21 | background-color: #e0e0e0; 22 | } 23 | 24 | .chat-header { 25 | display: flex; 26 | justify-content: space-between; 27 | align-items: center; 28 | background-color: #fff; 29 | padding: 10px; 30 | border-bottom: 4px solid #000; 31 | } 32 | 33 | .chat-header div { 34 | display: flex; 35 | align-items: center; 36 | } 37 | 38 | .chat-header label { 39 | margin-right: 5px; 40 | } 41 | 42 | .chat-header button { 43 | margin-left: 10px; 44 | padding: 5px 10px; 45 | border: 2px solid #000; 46 | background-color: #ff6347; 47 | color: #000; 48 | cursor: pointer; 49 | } 50 | 51 | .chat-header button:hover { 52 | background-color: #ff856d; 53 | } 54 | 55 | .chat-messages { 56 | flex: 1; 57 | padding: 10px; 58 | overflow-y: auto; 59 | background-color: #d0d0d0; 60 | border: 2px solid #000; 61 | } 62 | 63 | .chat-input { 64 | display: flex; 65 | padding: 10px; 66 | border-top: 4px solid #000; 67 | background-color: #fff; 68 | } 69 | 70 | .chat-input input { 71 | flex: 1; 72 | padding: 10px; 73 | border: 2px solid #000; 74 | background-color: #dcdcdc; 75 | color: #000; 76 | } 77 | 78 | .chat-input button { 79 | padding: 10px 20px; 80 | border: 2px solid #000; 81 | background-color: #32cd32; 82 | color: #000; 83 | cursor: pointer; 84 | margin-left: 10px; 85 | } 86 | 87 | .chat-input button:hover { 88 | background-color: #66ff66; 89 | } 90 | 91 | .message { 92 | margin-bottom: 10px; 93 | padding: 10px; 94 | border: 2px solid #000; 95 | background-color: #cccccc; 96 | } 97 | 98 | .message.user { 99 | align-self: flex-end; 100 | background-color: #4682b4; 101 | color: #fff; 102 | } 103 | 104 | .message.assistant { 105 | align-self: flex-start; 106 | background-color: #98fb98; 107 | color: #000; 108 | } 109 | 110 | .message.tool { 111 | align-self: center; 112 | background-color: #ffd700; 113 | color: #000; 114 | } 115 | 116 | /* New styles for the index.html page */ 117 | .center-container { 118 | display: flex; 119 | flex-direction: column; 120 | justify-content: center; 121 | align-items: center; 122 | height: 100vh; 123 | text-align: center; 124 | } 125 | 126 | .center-container h1 { 127 | font-size: 5rem; 128 | margin: 0; 129 | } 130 | 131 | .center-container p { 132 | font-size: 1.5rem; 133 | margin: 0; 134 | color: #ff6347; 135 | } 136 | 137 | footer { 138 | margin-top: 20px; 139 | text-align: center; 140 | font-size: 1rem; 141 | } 142 | 143 | footer a { 144 | color: #ff6347; 145 | text-decoration: none; 146 | } 147 | 148 | footer a:hover { 149 | text-decoration: underline; 150 | } 151 | -------------------------------------------------------------------------------- /wrangler.toml: -------------------------------------------------------------------------------- 1 | #:schema node_modules/wrangler/config-schema.json 2 | name = "shrty-dot-dev" 3 | main = "src/index.ts" 4 | compatibility_date = "2024-09-28" 5 | compatibility_flags = ["nodejs_compat"] 6 | [assets] 7 | directory = "./public" 8 | 9 | routes = [ 10 | { pattern = "shrty.dev", custom_domain = true } 11 | ] 12 | 13 | 14 | # Automatically place your workloads in an optimal location to minimize latency. 15 | # If you are running back-end logic in a Worker, running it closer to your back-end infrastructure 16 | # rather than the end user may result in better performance. 17 | # Docs: https://developers.cloudflare.com/workers/configuration/smart-placement/#smart-placement 18 | # [placement] 19 | # mode = "smart" 20 | 21 | # Variable bindings. These are arbitrary, plaintext strings (similar to environment variables) 22 | # Docs: 23 | # - https://developers.cloudflare.com/workers/wrangler/configuration/#environment-variables 24 | # Note: Use secrets to store sensitive data. 25 | # - https://developers.cloudflare.com/workers/configuration/secrets/ 26 | # [vars] 27 | # MY_VARIABLE = "production_value" 28 | 29 | # Bind the Workers AI model catalog. Run machine learning models, powered by serverless GPUs, on Cloudflare’s global network 30 | # Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#workers-ai 31 | [ai] 32 | binding = "AI" 33 | 34 | # Bind an Analytics Engine dataset. Use Analytics Engine to write analytics within your Pages Function. 35 | # Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#analytics-engine-datasets 36 | [[analytics_engine_datasets]] 37 | binding = "TRACKER" 38 | dataset = "link_clicks" 39 | 40 | # Bind a headless browser instance running on Cloudflare's global network. 41 | # Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#browser-rendering 42 | # [browser] 43 | # binding = "MY_BROWSER" 44 | 45 | # Bind a D1 database. D1 is Cloudflare’s native serverless SQL database. 46 | # Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#d1-databases 47 | # [[d1_databases]] 48 | # binding = "MY_DB" 49 | # database_name = "my-database" 50 | # database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" 51 | 52 | # Bind a dispatch namespace. Use Workers for Platforms to deploy serverless functions programmatically on behalf of your customers. 53 | # Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#dispatch-namespace-bindings-workers-for-platforms 54 | # [[dispatch_namespaces]] 55 | # binding = "MY_DISPATCHER" 56 | # namespace = "my-namespace" 57 | 58 | # Bind a Durable Object. Durable objects are a scale-to-zero compute primitive based on the actor model. 59 | # Durable Objects can live for as long as needed. Use these when you need a long-running "server", such as in realtime apps. 60 | # Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#durable-objects 61 | # [[durable_objects.bindings]] 62 | # name = "MY_DURABLE_OBJECT" 63 | # class_name = "MyDurableObject" 64 | 65 | # Durable Object migrations. 66 | # Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#migrations 67 | # [[migrations]] 68 | # tag = "v1" 69 | # new_classes = ["MyDurableObject"] 70 | 71 | # Bind a Hyperdrive configuration. Use to accelerate access to your existing databases from Cloudflare Workers. 72 | # Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#hyperdrive 73 | # [[hyperdrive]] 74 | # binding = "MY_HYPERDRIVE" 75 | # id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" 76 | 77 | # Bind a KV Namespace. Use KV as persistent storage for small key-value pairs. 78 | # Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#kv-namespaces 79 | [[kv_namespaces]] 80 | binding = "URLS" 81 | id = "d76837429fda4e0bbeb25ab742880a40" 82 | 83 | # Bind an mTLS certificate. Use to present a client certificate when communicating with another service. 84 | # Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#mtls-certificates 85 | # [[mtls_certificates]] 86 | # binding = "MY_CERTIFICATE" 87 | # certificate_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" 88 | 89 | # Bind a Queue producer. Use this binding to schedule an arbitrary task that may be processed later by a Queue consumer. 90 | # Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#queues 91 | # [[queues.producers]] 92 | # binding = "MY_QUEUE" 93 | # queue = "my-queue" 94 | 95 | # Bind a Queue consumer. Queue Consumers can retrieve tasks scheduled by Producers to act on them. 96 | # Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#queues 97 | # [[queues.consumers]] 98 | # queue = "my-queue" 99 | 100 | # Bind an R2 Bucket. Use R2 to store arbitrarily large blobs of data, such as files. 101 | # Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#r2-buckets 102 | # [[r2_buckets]] 103 | # binding = "MY_BUCKET" 104 | # bucket_name = "my-bucket" 105 | 106 | # Bind another Worker service. Use this binding to call another Worker without network overhead. 107 | # Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#service-bindings 108 | # [[services]] 109 | # binding = "MY_SERVICE" 110 | # service = "my-service" 111 | 112 | # Bind a Vectorize index. Use to store and query vector embeddings for semantic search, classification and other vector search use-cases. 113 | # Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#vectorize-indexes 114 | # [[vectorize]] 115 | # binding = "MY_INDEX" 116 | # index_name = "my-index" 117 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { runWithTools } from '@cloudflare/ai-utils'; 2 | import { Hono } from 'hono'; 3 | import { jwt, sign } from 'hono/jwt'; 4 | import { stripIndents } from 'common-tags'; 5 | import { streamText } from 'hono/streaming'; 6 | import { events } from 'fetch-event-stream'; 7 | import { coerceBoolean } from 'cloudflare/core.mjs'; 8 | 9 | type Bindings = { 10 | [key in keyof CloudflareBindings]: CloudflareBindings[key]; 11 | }; 12 | 13 | const app = new Hono<{ Bindings: Bindings }>(); 14 | 15 | // Secure all the API routes 16 | app.use('/api/*', (c, next) => { 17 | const jwtMiddleware = jwt({ 18 | secret: c.env.JWT_SECRET, 19 | }); 20 | return jwtMiddleware(c, next); 21 | }); 22 | 23 | // Generate a signed token 24 | app.post("/tmp/token", async (c) => { 25 | const payload = await c.req.json(); 26 | console.log({payload}); 27 | const token = await sign(payload, c.env.JWT_SECRET); 28 | return c.json({token}); 29 | }); 30 | 31 | async function addUrl(env: Bindings, slug: string, url: string, override: boolean = false) { 32 | const existing = await env.URLS.get(slug); 33 | console.log({ slug, url, override }); 34 | if (existing !== null) { 35 | if (coerceBoolean(override) === true) { 36 | console.log(`Overriding shorty ${slug}`); 37 | } else { 38 | return { 39 | slug, 40 | url: existing, 41 | shorty: `/${slug}`, 42 | message: `Did not update ${slug} because it already was pointing to ${existing} and override was set to ${override}.`, 43 | }; 44 | } 45 | } 46 | await env.URLS.put(slug, url); 47 | return { slug, url, shorty: `/${slug}` }; 48 | } 49 | 50 | app.post('/api/url', async (c) => { 51 | const payload = await c.req.json(); 52 | const result = await addUrl(c.env, payload.slug, payload.url, payload.override); 53 | return c.json(result); 54 | }); 55 | 56 | async function queryClicks(env: Bindings, sql: string) { 57 | console.log(sql); 58 | const API = `https://api.cloudflare.com/client/v4/accounts/${env.CLOUDFLARE_ACCOUNT_ID}/analytics_engine/sql`; 59 | const response = await fetch(API, { 60 | method: 'POST', 61 | headers: { 62 | Authorization: `Bearer ${env.CLOUDFLARE_API_TOKEN}`, 63 | }, 64 | body: sql, 65 | }); 66 | const jsonResponse = await response.json(); 67 | // @ts-ignore 68 | return jsonResponse.data; 69 | } 70 | 71 | app.post('/api/report/:slug', async (c) => { 72 | const sql = `SELECT blob4 as 'country', COUNT() as 'total' FROM link_clicks WHERE blob1='${c.req.param('slug')}' GROUP BY country`; 73 | const results = await queryClicks(c.env, sql); 74 | return c.json(results); 75 | }); 76 | 77 | // TODO: Remove temporary hack 78 | const SHORTY_SYSTEM_MESSAGE = stripIndents` 79 | You are an assistant for the URL Shortening service named shrty.dev. 80 | 81 | Each shortened link is called a shorty. Each shorty starts with the current hostname and then is followed by a forward slash and then the slug. 82 | 83 | You are jovial and want to encourage people to create great shortened links. 84 | 85 | When doing function calling ensure that boolean values are ALWAYS lowercased, eg: instead of True use true. 86 | `; 87 | 88 | 89 | app.post('/admin/chat', async (c) => { 90 | const payload = await c.req.json(); 91 | const messages = payload.messages || []; 92 | //console.log({ submittedMessages: messages }); 93 | messages.unshift({ 94 | role: 'system', 95 | content: SHORTY_SYSTEM_MESSAGE, 96 | }); 97 | 98 | const eventSourceStream = await runWithTools( 99 | c.env.AI, 100 | '@hf/nousresearch/hermes-2-pro-mistral-7b', 101 | { 102 | messages, 103 | tools: [ 104 | { 105 | name: 'createShorty', 106 | description: 'Creates a new short link', 107 | parameters: { 108 | type: 'object', 109 | properties: { 110 | slug: { 111 | type: 'string', 112 | description: 'The shortened part of the url.', 113 | }, 114 | url: { 115 | type: 'string', 116 | description: 'The final destination where the shorty should redirect. Should start with https://', 117 | }, 118 | override: { 119 | type: 'boolean', 120 | description: 121 | 'Will override if there is an existing shorty at that slug. Default is false.', 122 | }, 123 | }, 124 | required: ['slug', 'url'], 125 | }, 126 | function: async ({ slug, url, override }) => { 127 | const result = await addUrl(c.env, slug, url, override); 128 | return JSON.stringify(result); 129 | }, 130 | }, 131 | { 132 | name: 'getClicksByCountryReport', 133 | description: 'Returns a report of all clicks on a specific shorty grouped by country', 134 | parameters: { 135 | type: 'object', 136 | properties: { 137 | slug: { 138 | type: 'string', 139 | description: 'The shortened part of the url', 140 | }, 141 | }, 142 | required: ['slug'], 143 | }, 144 | function: async ({ slug }) => { 145 | const sql = stripIndents` 146 | SELECT 147 | blob4 as 'country', 148 | COUNT() as 'total' 149 | FROM 150 | link_clicks 151 | WHERE blob1='${slug}' 152 | GROUP BY country`; 153 | const result = await queryClicks(c.env, sql); 154 | return JSON.stringify(result); 155 | }, 156 | }, 157 | ], 158 | }, 159 | { 160 | streamFinalResponse: true, 161 | verbose: true, 162 | } 163 | ); 164 | 165 | return streamText(c, async (stream) => { 166 | const chunks = events(new Response(eventSourceStream as ReadableStream)); 167 | for await (const chunk of chunks) { 168 | if (chunk.data && chunk.data !== '[DONE]' && chunk.data !== '<|im_end|>') { 169 | const data = JSON.parse(chunk.data); 170 | stream.write(data.response); 171 | } 172 | } 173 | }); 174 | }); 175 | 176 | app.get('/:slug', async (c) => { 177 | const slug = c.req.param('slug'); 178 | const url = await c.env.URLS.get(slug); 179 | if (url === null) { 180 | return c.status(404); 181 | } 182 | const cfProperties = c.req.raw.cf; 183 | if (cfProperties !== undefined) { 184 | if (c.env.TRACKER !== undefined) { 185 | c.env.TRACKER.writeDataPoint({ 186 | blobs: [ 187 | slug as string, 188 | url as string, 189 | cfProperties.city as string, 190 | cfProperties.country as string, 191 | cfProperties.continent as string, 192 | cfProperties.region as string, 193 | cfProperties.regionCode as string, 194 | cfProperties.timezone as string, 195 | ], 196 | doubles: [cfProperties.metroCode as number, cfProperties.longitude as number, cfProperties.latitude as number], 197 | indexes: [slug as string], 198 | }); 199 | } else { 200 | console.warn(`TRACKER not defined (does not work on local dev), passing through ${slug} to ${url}`); 201 | } 202 | } 203 | // Redirect 204 | return c.redirect(url); 205 | }); 206 | 207 | export default app; 208 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2021" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 15 | "lib": ["es2021"] /* Specify a set of bundled library declaration files that describe the target runtime environment. */, 16 | "jsx": "react-jsx" /* Specify what JSX code is generated. */, 17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ 22 | // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | 26 | /* Modules */ 27 | "module": "es2022" /* Specify what module code is generated. */, 28 | // "rootDir": "./", /* Specify the root folder within your source files. */ 29 | "moduleResolution": "Bundler" /* Specify how TypeScript looks up a file from a given module specifier. */, 30 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 31 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 32 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 33 | // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ 34 | "types": [ 35 | "@cloudflare/workers-types/2023-07-01" 36 | ] /* Specify type package names to be included without being referenced in a source file. */, 37 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 38 | "resolveJsonModule": true /* Enable importing .json files */, 39 | // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ 40 | 41 | /* JavaScript Support */ 42 | "allowJs": true /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */, 43 | "checkJs": false /* Enable error reporting in type-checked JavaScript files. */, 44 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ 45 | 46 | /* Emit */ 47 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 48 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 49 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 50 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 51 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ 52 | // "outDir": "./", /* Specify an output folder for all emitted files. */ 53 | // "removeComments": true, /* Disable emitting comments. */ 54 | "noEmit": true /* Disable emitting files from a compilation. */, 55 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 56 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ 57 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 58 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 59 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 60 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 61 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 62 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 63 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 64 | // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ 65 | // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ 66 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 67 | // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ 68 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 69 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 70 | 71 | /* Interop Constraints */ 72 | "isolatedModules": true /* Ensure that each file can be safely transpiled without relying on other imports. */, 73 | "allowSyntheticDefaultImports": true /* Allow 'import x from y' when a module doesn't have a default export. */, 74 | // "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */, 75 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 76 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 77 | 78 | /* Type Checking */ 79 | "strict": true /* Enable all strict type-checking options. */, 80 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ 81 | // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ 82 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 83 | // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ 84 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 85 | // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ 86 | // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ 87 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 88 | // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ 89 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ 90 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 91 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 92 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 93 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 94 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 95 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ 96 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 97 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 98 | 99 | /* Completeness */ 100 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 101 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 102 | }, 103 | "exclude": ["test"] 104 | } 105 | --------------------------------------------------------------------------------