├── 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 |
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 | [](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 |
--------------------------------------------------------------------------------