ReviewOS

also looking at this

stacks/uplink

feat(homepage): say what a text can actually do

#64
Merged glennmichael123 wants to merge feat/say-what-a-text-can-do into main
11 files +149 -22
.env.examplemodified+3-0
Changes to .env.example
@@ -155,6 +155,9 @@ UPLINK_CLAUDE_MODEL=
155155UPLINK_MODEL=
156156# Optional: where the claude binary is, if it is not on PATH.
157157UPLINK_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.
160UPLINK_PERMISSION_MODE=bypassPermissions
158161# --- Codex ---
159162# Sign in with `codex login` (or `codex login --device-auth` over SSH). Codex keeps the
160163# credential itself, under CODEX_HOME, so there is no token to put here.
config/analytics.tsmodified+11-13
Changes to config/analytics.ts
@@ -3,20 +3,18 @@ import type { AnalyticsConfig } from '@stacksjs/types'
33/**
44 * **Analytics Configuration**
55 *
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.
915 */
1016export default {
17 enabled: false,
1118 driver: 'fathom',
12
13 drivers: {
14 googleAnalytics: {
15 trackingId: 'UA-XXXXXXXXX-X',
16 },
17
18 fathom: {
19 siteId: 'WOLZMJDL',
20 },
21 },
19 drivers: {},
2220} satisfies AnalyticsConfig
public/fonts/geist-latin-ext.woff2added+0-0

Binary file. Nothing to show line by line.

public/fonts/geist-latin.woff2added+0-0

Binary file. Nothing to show line by line.

public/fonts/geist-mono-latin-ext.woff2added+0-0

Binary file. Nothing to show line by line.

public/fonts/geist-mono-latin.woff2added+0-0

Binary file. Nothing to show line by line.

resources/partials/site-head.stxmodified+40-3
Changes to resources/partials/site-head.stx
@@ -76,10 +76,47 @@ const seo = {
7676<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
7777<link rel="apple-touch-icon" href="/apple-touch-icon.png">
7878<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>
8286<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
83120 /* Tokens. One accent: sodium amber, the colour of the city lights in the photos. */
84121 :root {
85122 --paper: #fafaf9;
resources/partials/site-nav.stxmodified+1-0
Changes to resources/partials/site-nav.stx
@@ -14,6 +14,7 @@ const navDownloadUrl = release.url
1414 </a>
1515 <div class="flex gap-5 items-center text-sm sm:gap-6">
1616 <a href="/#how" class="hidden sm:inline hover:opacity-80 u-muted">How it works</a>
17 <a href="/#security" class="hidden lg:inline hover:opacity-80 u-muted">Security</a>
1718 @if(path === '/pricing')
1819 <a href="/pricing" class="hover:opacity-80 u-muted" aria-current="page">Pricing</a>
1920 @else
resources/views/404.stxmodified+35-3
Changes to resources/views/404.stx
@@ -24,10 +24,42 @@ notFound()
2424 <link rel="icon" href="/favicon.ico" sizes="48x48">
2525 <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
2626 <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>
3029 <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
3163 :root { --paper: #fafaf9; --ink: #18181b; --muted: #52525b; --accent: #f59e0b; --accent-ink: #b45309; }
3264 @media (prefers-color-scheme: dark) { :root { --paper: #0c0c0e; --ink: #f4f4f5; --muted: #a1a1aa; --accent-ink: #fbbf24; } }
3365 html body.uplink { background: var(--paper); color: var(--ink); font-family: 'Geist', ui-sans-serif, system-ui, sans-serif; }
resources/views/index.stxmodified+58-2
Changes to resources/views/index.stx
@@ -73,6 +73,21 @@ const install = [
7373 { verb: 'Text it', body: 'Send yourself "ping" from your iPhone. The answer starts with a satellite.' },
7474]
7575
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.
78const 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
84const 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
7691const pricing = PLANS.map(plan => ({ ...plan, price: formatCents(plan.priceCents) }))
7792const yearlySaving = yearlySavingPercent()
7893
@@ -186,7 +201,7 @@ const faqs = [
186201 <div class="lg:sticky lg:top-28 lg:self-start">
187202 <p class="text-xs tracking-[0.18em] uppercase u-accent u-mono">How it works</p>
188203 <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>
190205 </div>
191206 <ol class="grid gap-10">
192207 @foreach(steps as step)
@@ -250,13 +265,54 @@ const faqs = [
250265 </span>
251266 <div>
252267 <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>
254269 </div>
255270 </article>
256271 </div>
257272 </div>
258273 </section>
259274
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
260316 <section id="download" class="border-t scroll-mt-16 u-line" aria-labelledby="download-title">
261317 <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">
262318 <div>
resources/views/privacy.stxmodified+1-1
Changes to resources/views/privacy.stx
@@ -23,7 +23,7 @@ const sections = [
2323 heading: 'What stays on your Mac',
2424 paragraphs: [
2525 '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.',
2727 ],
2828 },
2929 {