also looking at this
feat(homepage): say what a text can actually do
#64
11 files
+149
-22
| @@ -155,6 +155,9 @@ UPLINK_CLAUDE_MODEL= | ||
| 155 | 155 | UPLINK_MODEL= |
| 156 | 156 | # Optional: where the claude binary is, if it is not on PATH. |
| 157 | 157 | UPLINK_CLAUDE_BIN= |
| 158 | # How much Claude Code may do without asking. `bypassPermissions` because | |
| 159 | # nobody is at the Mac to answer a prompt. Codex's equivalent is below. | |
| 160 | UPLINK_PERMISSION_MODE=bypassPermissions | |
| 158 | 161 | # --- Codex --- |
| 159 | 162 | # Sign in with `codex login` (or `codex login --device-auth` over SSH). Codex keeps the |
| 160 | 163 | # credential itself, under CODEX_HOME, so there is no token to put here. |
| @@ -3,20 +3,18 @@ import type { AnalyticsConfig } from '@stacksjs/types' | ||
| 3 | 3 | /** |
| 4 | 4 | * **Analytics Configuration** |
| 5 | 5 | * |
| 6 | * This configuration defines all of your Analytics options. Because Stacks is fully-typed, | |
| 7 | * you may hover any of the options below and the definitions will be provided. In case | |
| 8 | * you have any questions, feel free to reach out via Discord or GitHub Discussions. | |
| 6 | * Off, and it always was: nothing on this site injects an analytics script. | |
| 7 | * | |
| 8 | * The scaffold left `driver: 'fathom'` here with a real site id and a Google | |
| 9 | * Analytics placeholder beside it, so anyone who read the config found a | |
| 10 | * tracker declared while `/privacy` told them the site loads none. Both | |
| 11 | * statements cannot be true, and the page is the one that is (#37). | |
| 12 | * | |
| 13 | * Turning this on means adding the script as well, and saying so on the | |
| 14 | * privacy page in the same commit. | |
| 9 | 15 | */ |
| 10 | 16 | export default { |
| 17 | enabled: false, | |
| 11 | 18 | driver: 'fathom', |
| 12 | ||
| 13 | drivers: { | |
| 14 | googleAnalytics: { | |
| 15 | trackingId: 'UA-XXXXXXXXX-X', | |
| 16 | }, | |
| 17 | ||
| 18 | fathom: { | |
| 19 | siteId: 'WOLZMJDL', | |
| 20 | }, | |
| 21 | }, | |
| 19 | drivers: {}, | |
| 22 | 20 | } satisfies AnalyticsConfig |
Binary file. Nothing to show line by line.
Binary file. Nothing to show line by line.
Binary file. Nothing to show line by line.
Binary file. Nothing to show line by line.
| @@ -76,10 +76,47 @@ const seo = { | ||
| 76 | 76 | <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png"> |
| 77 | 77 | <link rel="apple-touch-icon" href="/apple-touch-icon.png"> |
| 78 | 78 | <link rel="manifest" href="/site.webmanifest"> |
| 79 | <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| 80 | <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| 81 | <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Geist:wght@400..700&family=Geist+Mono:wght@400..600&display=swap"> | |
| 79 | <!-- Served from here rather than from Google. The stylesheet used to be | |
| 80 | fetched from Google Fonts, with a preconnect to its CDN, so every | |
| 81 | visitor's IP address and browser reached Google before a word rendered, | |
| 82 | on a site whose privacy page is about your texts staying yours. Two | |
| 83 | families, latin and latin-ext, 92 KB in total. --> | |
| 84 | <link rel="preload" href="/fonts/geist-latin.woff2" as="font" type="font/woff2" crossorigin> | |
| 85 | <link rel="preload" href="/fonts/geist-mono-latin.woff2" as="font" type="font/woff2" crossorigin> | |
| 82 | 86 | <style> |
| 87 | @font-face { | |
| 88 | font-family: "Geist"; | |
| 89 | font-style: normal; | |
| 90 | font-weight: 400 700; | |
| 91 | font-display: swap; | |
| 92 | src: url("/fonts/geist-latin-ext.woff2") format("woff2"); | |
| 93 | unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; | |
| 94 | } | |
| 95 | @font-face { | |
| 96 | font-family: "Geist"; | |
| 97 | font-style: normal; | |
| 98 | font-weight: 400 700; | |
| 99 | font-display: swap; | |
| 100 | src: url("/fonts/geist-latin.woff2") format("woff2"); | |
| 101 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; | |
| 102 | } | |
| 103 | @font-face { | |
| 104 | font-family: "Geist Mono"; | |
| 105 | font-style: normal; | |
| 106 | font-weight: 400 600; | |
| 107 | font-display: swap; | |
| 108 | src: url("/fonts/geist-mono-latin-ext.woff2") format("woff2"); | |
| 109 | unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; | |
| 110 | } | |
| 111 | @font-face { | |
| 112 | font-family: "Geist Mono"; | |
| 113 | font-style: normal; | |
| 114 | font-weight: 400 600; | |
| 115 | font-display: swap; | |
| 116 | src: url("/fonts/geist-mono-latin.woff2") format("woff2"); | |
| 117 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; | |
| 118 | } | |
| 119 | ||
| 83 | 120 | /* Tokens. One accent: sodium amber, the colour of the city lights in the photos. */ |
| 84 | 121 | :root { |
| 85 | 122 | --paper: #fafaf9; |
| @@ -24,10 +24,42 @@ notFound() | ||
| 24 | 24 | <link rel="icon" href="/favicon.ico" sizes="48x48"> |
| 25 | 25 | <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png"> |
| 26 | 26 | <link rel="apple-touch-icon" href="/apple-touch-icon.png"> |
| 27 | <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| 28 | <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| 29 | <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Geist:wght@400..700&display=swap"> | |
| 27 | <!-- Self-hosted, like the shared head. This page has its own. --> | |
| 28 | <link rel="preload" href="/fonts/geist-latin.woff2" as="font" type="font/woff2" crossorigin> | |
| 30 | 29 | <style> |
| 30 | @font-face { | |
| 31 | font-family: "Geist"; | |
| 32 | font-style: normal; | |
| 33 | font-weight: 400 700; | |
| 34 | font-display: swap; | |
| 35 | src: url("/fonts/geist-latin-ext.woff2") format("woff2"); | |
| 36 | unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; | |
| 37 | } | |
| 38 | @font-face { | |
| 39 | font-family: "Geist"; | |
| 40 | font-style: normal; | |
| 41 | font-weight: 400 700; | |
| 42 | font-display: swap; | |
| 43 | src: url("/fonts/geist-latin.woff2") format("woff2"); | |
| 44 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; | |
| 45 | } | |
| 46 | @font-face { | |
| 47 | font-family: "Geist Mono"; | |
| 48 | font-style: normal; | |
| 49 | font-weight: 400 600; | |
| 50 | font-display: swap; | |
| 51 | src: url("/fonts/geist-mono-latin-ext.woff2") format("woff2"); | |
| 52 | unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; | |
| 53 | } | |
| 54 | @font-face { | |
| 55 | font-family: "Geist Mono"; | |
| 56 | font-style: normal; | |
| 57 | font-weight: 400 600; | |
| 58 | font-display: swap; | |
| 59 | src: url("/fonts/geist-mono-latin.woff2") format("woff2"); | |
| 60 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; | |
| 61 | } | |
| 62 | ||
| 31 | 63 | :root { --paper: #fafaf9; --ink: #18181b; --muted: #52525b; --accent: #f59e0b; --accent-ink: #b45309; } |
| 32 | 64 | @media (prefers-color-scheme: dark) { :root { --paper: #0c0c0e; --ink: #f4f4f5; --muted: #a1a1aa; --accent-ink: #fbbf24; } } |
| 33 | 65 | html body.uplink { background: var(--paper); color: var(--ink); font-family: 'Geist', ui-sans-serif, system-ui, sans-serif; } |
| @@ -73,6 +73,21 @@ const install = [ | ||
| 73 | 73 | { verb: 'Text it', body: 'Send yourself "ping" from your iPhone. The answer starts with a satellite.' }, |
| 74 | 74 | ] |
| 75 | 75 | |
| 76 | // What a text can actually do, and what cannot reach it. The page asks for | |
| 77 | // Full Disk Access and an unattended agent, and said nothing about either. | |
| 78 | const power = [ | |
| 79 | { title: 'Approvals are off', body: 'Nobody is at the Mac to answer a prompt, so there are none. Claude Code runs with bypassPermissions, Codex with bypass.' }, | |
| 80 | { title: 'It can read, write and run', body: 'Inside the directory your text names, with your permissions. The same authority you have at a terminal on that Mac.' }, | |
| 81 | { title: 'So your phone is the key', body: 'An unlocked phone with access to the allowed thread can command your Mac. Treat the allow list the way you treat an SSH key.' }, | |
| 82 | ] | |
| 83 | ||
| 84 | const limits = [ | |
| 85 | { title: 'iMessage only', body: 'SMS is refused outright. A forwarded SMS sender id can be spoofed through a bulk gateway, and the other side of that door is an agent with approvals off.' }, | |
| 86 | { title: 'One to one only', body: 'A group chat never reaches it, whoever is in it.' }, | |
| 87 | { title: 'Handles you allow', body: 'Empty means this Mac\'s own Messages handles, so a default install answers only you.' }, | |
| 88 | { title: 'Nothing stale', body: 'A text older than the catch-up window is not run, so a Mac waking up cannot replay a day of instructions.' }, | |
| 89 | ] | |
| 90 | ||
| 76 | 91 | const pricing = PLANS.map(plan => ({ ...plan, price: formatCents(plan.priceCents) })) |
| 77 | 92 | const yearlySaving = yearlySavingPercent() |
| 78 | 93 | |
| @@ -186,7 +201,7 @@ const faqs = [ | ||
| 186 | 201 | <div class="lg:sticky lg:top-28 lg:self-start"> |
| 187 | 202 | <p class="text-xs tracking-[0.18em] uppercase u-accent u-mono">How it works</p> |
| 188 | 203 | <h2 id="how-title" class="mt-3 font-semibold text-3xl tracking-tight md:text-4xl">From a text to an agent session, and back.</h2> |
| 189 | <p class="mt-4 max-w-[46ch] u-muted">No server in between. Your Mac reads the message, does the work and answers through Messages.</p> | |
| 204 | <p class="mt-4 max-w-[46ch] u-muted">Your Mac reads the message, does the work and answers through Messages. The agent sends your prompt to Anthropic or OpenAI under your own account, and <a href="/privacy" class="underline underline-offset-4 hover:opacity-80">nothing else leaves</a>.</p> | |
| 190 | 205 | </div> |
| 191 | 206 | <ol class="grid gap-10"> |
| 192 | 207 | @foreach(steps as step) |
| @@ -250,13 +265,54 @@ const faqs = [ | ||
| 250 | 265 | </span> |
| 251 | 266 | <div> |
| 252 | 267 | <h3 class="font-semibold text-xl">Nobody else can use it</h3> |
| 253 | <p class="mt-1 leading-relaxed u-muted">Only one-to-one chats with handles you allow. Group chats are ignored, and only Uplink.app gets Full Disk Access, not your terminal.</p> | |
| 268 | <p class="mt-1 leading-relaxed u-muted">Only one-to-one chats with handles you allow. Group chats are ignored, and only Uplink.app gets Full Disk Access, not your terminal. <a href="#security" class="underline underline-offset-4 hover:opacity-80">What a text can do</a>.</p> | |
| 254 | 269 | </div> |
| 255 | 270 | </article> |
| 256 | 271 | </div> |
| 257 | 272 | </div> |
| 258 | 273 | </section> |
| 259 | 274 | |
| 275 | <section id="security" class="border-t scroll-mt-16 u-line" aria-labelledby="security-title"> | |
| 276 | <div class="mx-auto px-4 py-20 sm:px-6 lg:py-28 max-w-6xl"> | |
| 277 | <p class="text-xs tracking-[0.18em] uppercase u-accent u-mono">Before you install it</p> | |
| 278 | <h2 id="security-title" class="mt-3 max-w-[20ch] font-semibold text-3xl tracking-tight md:text-4xl">A text runs a real agent, unattended.</h2> | |
| 279 | <p class="mt-4 max-w-[58ch] u-muted"> | |
| 280 | That is the product, and it is worth knowing before you point it at a Mac that matters. | |
| 281 | </p> | |
| 282 | ||
| 283 | <div class="grid gap-12 mt-14 lg:grid-cols-2 lg:gap-16"> | |
| 284 | <div> | |
| 285 | <h3 class="font-semibold text-lg">What a text can do</h3> | |
| 286 | <ul class="grid gap-5 mt-6"> | |
| 287 | @foreach(power as item) | |
| 288 | <li class="u-reveal"> | |
| 289 | <p class="font-medium">{{ item.title }}</p> | |
| 290 | <p class="mt-1 max-w-[52ch] leading-relaxed u-muted">{{ item.body }}</p> | |
| 291 | </li> | |
| 292 | @endforeach | |
| 293 | </ul> | |
| 294 | </div> | |
| 295 | ||
| 296 | <div> | |
| 297 | <h3 class="font-semibold text-lg">What cannot reach it</h3> | |
| 298 | <ul class="grid gap-5 mt-6"> | |
| 299 | @foreach(limits as item) | |
| 300 | <li class="u-reveal"> | |
| 301 | <p class="font-medium">{{ item.title }}</p> | |
| 302 | <p class="mt-1 max-w-[52ch] leading-relaxed u-muted">{{ item.body }}</p> | |
| 303 | </li> | |
| 304 | @endforeach | |
| 305 | </ul> | |
| 306 | </div> | |
| 307 | </div> | |
| 308 | ||
| 309 | <p class="mt-14 max-w-[58ch] u-muted"> | |
| 310 | Your prompt and whatever the agent reads go to Anthropic or OpenAI under your own account. Your texts go through Apple, as any iMessage does. A licence check reaches uplink.stacksjs.com every six hours and sends the key, nothing else. | |
| 311 | <a href="/privacy" class="underline underline-offset-4 hover:opacity-80">The whole of it is on the privacy page</a>. | |
| 312 | </p> | |
| 313 | </div> | |
| 314 | </section> | |
| 315 | ||
| 260 | 316 | <section id="download" class="border-t scroll-mt-16 u-line" aria-labelledby="download-title"> |
| 261 | 317 | <div class="grid gap-12 items-center lg:gap-16 lg:grid-cols-[1.1fr_0.9fr] mx-auto px-4 py-20 sm:px-6 lg:py-28 max-w-6xl"> |
| 262 | 318 | <div> |
| @@ -23,7 +23,7 @@ const sections = [ | ||
| 23 | 23 | heading: 'What stays on your Mac', |
| 24 | 24 | paragraphs: [ |
| 25 | 25 | 'Uplink reads the Messages database on your Mac to find texts from the handles you allow, which is why it asks for Full Disk Access. It keeps its own record of each run (what was asked, the reply, how long it took) in a database on your Mac. You can see it in Uplink\'s dashboard.', |
| 26 | 'Your Claude Code token is stored in your Mac\'s Keychain, and so is your license key.', | |
| 26 | 'Your agent sign-in is stored in your Mac\'s Keychain, and so is your license key. Codex keeps its own credentials, so there is nothing of its for Uplink to hold.', | |
| 27 | 27 | ], |
| 28 | 28 | }, |
| 29 | 29 | { |