");
+ expect(releases[1].version).toBe("1.1.0");
+ });
+
+ it("falls back to the plain title when shortVersionString is absent", () => {
+ const xml = `${item({ title: "1.0.0" })}`;
+ const releases = parseAppcastItems(xml);
+ expect(releases[0].version).toBe("1.0.0");
+ expect(releases[0].build).toBeNull();
+ });
+
+ it("accepts non-CDATA escaped-HTML descriptions", () => {
+ const xml = `
+ ${item({ desc: "<p>Hello</p>" })}
+ `;
+ expect(parseAppcastItems(xml)[0].notesHtml).toBe("
Hello
");
+ });
+
+ it("returns [] for empty, non-XML, or item-less input", () => {
+ expect(parseAppcastItems("")).toEqual([]);
+ expect(parseAppcastItems("not an appcast")).toEqual([]);
+ expect(parseAppcastItems("")).toEqual([]);
+ });
+
+ it("skips items with no version signal at all", () => {
+ const xml = `
+ orphan
]]>
+ ${item({ title: "1.0.0" })}
+ `;
+ const releases = parseAppcastItems(xml);
+ expect(releases.length).toBe(1);
+ expect(releases[0].version).toBe("1.0.0");
+ });
+});
diff --git a/src/routes/nook/agents.tsx b/src/routes/nook/agents.tsx
new file mode 100644
index 0000000..565b245
--- /dev/null
+++ b/src/routes/nook/agents.tsx
@@ -0,0 +1,216 @@
+/**
+ * `/agents` on the Nook subdomain — supported agents and their event coverage.
+ *
+ * The registry below mirrors `AgentSpec.all` in the app
+ * (~/Code/the-nook/Sources/NookCore/Agents/AgentSpec.swift) and each agent's
+ * installed hook surface (hook/setup.go) or plugin event subscription
+ * (plugins/*), normalized onto the shared event vocabulary
+ * (~/Code/the-nook/docs/agent-interface.md). Copy follows
+ * marketing/positioning.md: category nouns, honest scoping, no unverifiable
+ * claims.
+ */
+import { For } from "solid-js";
+import { PageHead } from "~/components/PageHead";
+import { EdgeCacheHeaders } from "~/components/EdgeCacheHeaders";
+import SubdomainHeader from "~/components/SubdomainHeader";
+
+interface AgentEntry {
+ name: string;
+ /** How The Nook connects to this agent. */
+ wiring: string;
+ /** Events the installed hook/plugin reports, on the shared vocabulary. */
+ events: string[];
+ /** What the agent can be asked to do from a Nook card. */
+ gating: string;
+}
+
+const AGENTS: AgentEntry[] = [
+ {
+ name: "Claude Code",
+ wiring: "Hook entries in settings.json pointing at the shared hook binary",
+ events: [
+ "UserPromptSubmit", "SessionStart", "SessionEnd", "Stop", "StopFailure",
+ "SubagentStart", "SubagentStop", "Notification", "PreToolUse",
+ "PermissionRequest", "PostToolUse", "PostToolUseFailure",
+ "PermissionDenied", "PreCompact"
+ ],
+ gating: "Full — tool calls and permission prompts can be allowed or denied from a card"
+ },
+ {
+ name: "Codex",
+ wiring: "Hook entries in hooks.json (+ hooks enabled in config.toml)",
+ events: ["SessionStart", "UserPromptSubmit", "PermissionRequest", "Stop"],
+ gating: "Full — permission prompts can be allowed or denied from a card"
+ },
+ {
+ name: "OpenCode",
+ wiring: "In-process plugin (~/.config/opencode/plugins/nook.js) on the server bus",
+ events: [
+ "SessionStart", "SessionEnd", "UserPromptSubmit", "PreToolUse",
+ "PostToolUse", "Stop", "MessageUpdate", "PermissionRequest", "QuestionAsked"
+ ],
+ gating: "Full — the plugin answers OpenCode's own permission API when you tap Allow or Deny"
+ },
+ {
+ name: "Pi",
+ wiring: "In-process extension (~/.pi/agent/extensions/nook.ts)",
+ events: [
+ "SessionStart", "SessionEnd", "UserPromptSubmit", "PreToolUse",
+ "PostToolUse", "Stop", "MessageUpdate", "QuestionAsked", "QuestionResolved"
+ ],
+ gating: "Full when you force verification — Pi alone never asks natively, so the gate engages on the approval tool"
+ },
+ {
+ name: "Oh My Pi",
+ wiring: "In-process extension package (~/.omp/agent/extensions/nook/)",
+ events: [
+ "SessionStart", "SessionEnd", "UserPromptSubmit", "PreToolUse",
+ "PostToolUse", "Stop", "MessageUpdate", "QuestionAsked", "QuestionResolved"
+ ],
+ gating: "Full — honors the agent's own approval tiers, per-tool policies included"
+ },
+ {
+ name: "Hermes",
+ wiring: "Shell-hook entries in the agent's config, consent-gated by allowlist",
+ events: [
+ "SessionStart", "SessionEnd", "UserPromptSubmit", "Stop", "PreToolUse",
+ "PostToolUse", "SubagentStart", "SubagentStop"
+ ],
+ gating: "Observation — every event appears as a card; the agent keeps its own prompting"
+ },
+ {
+ name: "OpenClaw",
+ wiring: "Hook handler package (~/.openclaw/hooks/nook/)",
+ events: ["SessionStart", "UserPromptSubmit", "AgentResponse", "PreCompact", "SessionEnd"],
+ gating: "Observation — strict fire-and-forget; the agent surface has no tool or permission events"
+ },
+ {
+ name: "Z.ai (ZCode)",
+ wiring: "Hook entries in ~/.zcode/cli/config.json",
+ events: [
+ "SessionStart", "UserPromptSubmit", "PreToolUse", "PermissionRequest",
+ "PostToolUse", "PostToolUseFailure", "Stop"
+ ],
+ gating: "Full — tool calls and permission prompts can be allowed or denied from a card"
+ },
+ {
+ name: "Gemini CLI",
+ wiring: "Hook entries in settings.json",
+ events: ["SessionStart", "SessionEnd", "AgentStart", "Stop", "Notification"],
+ gating: "Observation — every event appears as a card; the agent keeps its own prompting"
+ },
+ {
+ name: "Cline",
+ wiring: "One hook file per event in ~/Documents/Cline/Hooks",
+ events: [
+ "SessionStart", "SessionEnd", "UserPromptSubmit", "PreToolUse",
+ "PostToolUse", "Stop", "PreCompact", "Notification"
+ ],
+ gating: "Observation — every event appears as a card; the agent keeps its own prompting"
+ },
+ {
+ name: "Qwen Code",
+ wiring: "Hook entries in settings.json (same surface as the Claude-style schema)",
+ events: [
+ "UserPromptSubmit", "SessionStart", "SessionEnd", "Stop", "StopFailure",
+ "SubagentStart", "SubagentStop", "Notification", "PreToolUse",
+ "PermissionRequest", "PostToolUse", "PostToolUseFailure",
+ "PermissionDenied", "PreCompact"
+ ],
+ gating: "Full — tool calls and permission prompts can be allowed or denied from a card"
+ },
+ {
+ name: "DeepSeek",
+ wiring: "Standalone hooks.json at ~/.dsh",
+ events: [
+ "SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse", "Stop",
+ "SubagentStart", "SubagentStop"
+ ],
+ gating: "Tool calls can be allowed or denied from a card; permission prompts stay with the agent"
+ }
+];
+
+const GATING_BADGE: Record = {
+ full: {
+ label: "Allow / Deny from a card",
+ class: "text-green border-green/40 bg-green/10"
+ },
+ observation: {
+ label: "Observe",
+ class: "text-blue border-blue/40 bg-blue/10"
+ }
+};
+
+function gatingKey(entry: AgentEntry): "full" | "observation" {
+ return entry.gating.startsWith("Full") ? "full" : "observation";
+}
+
+export default function NookAgentsPage() {
+ return (
+ <>
+
+
+
+
+
+ Supported agents
+
+
+ The Nook watches twelve coding agents in one place — your menu bar and
+ notch show every session that is running, thinking, or blocked, and a
+ card answers permission prompts without leaving your keyboard.
+
+
+ Each agent reports its own set of events. "Allow / Deny from a card"
+ means the agent hands the decision to The Nook while it waits;
+ "Observe" means every event still appears live, but the agent keeps
+ its own prompting. Agents can run on this Mac or on remote machines
+ over SSH — remote sessions report into the same grid.
+
+
+
+
+ {(agent) => (
+
+
+
{agent.name}
+
+ {GATING_BADGE[gatingKey(agent)].label}
+
+
+
{agent.wiring}
+
+ Events reported
+
+
+
+ {(event) => (
+
+ {event}
+
+ )}
+
+
+
{agent.gating}
+
+ )}
+
+
+
+
+ Running something else? A custom agent can wire itself in with the
+ same one-line contract the built-in integrations use — see the
+ plugin spec in the app's docs.
+
+
+ >
+ );
+}
diff --git a/src/routes/nook/changelog.tsx b/src/routes/nook/changelog.tsx
new file mode 100644
index 0000000..4dd4867
--- /dev/null
+++ b/src/routes/nook/changelog.tsx
@@ -0,0 +1,81 @@
+/**
+ * `/changelog` on the Nook subdomain — rendered from the Sparkle appcast.
+ *
+ * The appcast already ships HTML release notes to the updater on every
+ * launch, so this page renders exactly what Sparkle publishes: one source
+ * of truth, no second place to write release notes. Doubles as the raw
+ * material for the release-announcement job (marketing/README.md).
+ */
+import { For, Show } from "solid-js";
+import { query, createAsync } from "@solidjs/router";
+import { PageHead } from "~/components/PageHead";
+import { EdgeCacheHeaders } from "~/components/EdgeCacheHeaders";
+import SubdomainHeader from "~/components/SubdomainHeader";
+import { fetchAppcastReleases } from "~/routes/api/the-nook/_appcast";
+
+const getReleases = query(async () => {
+ "use server";
+ return fetchAppcastReleases();
+}, "nook-changelog-releases");
+
+export default function NookChangelogPage() {
+ const releases = createAsync(() => getReleases());
+
+ return (
+ <>
+
+
+
+
+
+ Changelog
+
+
+ What changed in each release. The app updates through Sparkle —
+ every entry here is what the updater shows.
+
+ {/* Notes are the publisher's own HTML, written to the same
+ S3 appcast the Sparkle updater consumes. */}
+
+
+ )}
+
+
+
+
+ >
+ );
+}
diff --git a/src/routes/nook/faq.tsx b/src/routes/nook/faq.tsx
new file mode 100644
index 0000000..087ee7c
--- /dev/null
+++ b/src/routes/nook/faq.tsx
@@ -0,0 +1,106 @@
+/**
+ * `/faq` on the Nook subdomain — answers to the objections that actually
+ * stop a purchase. Privacy scoping follows marketing/positioning.md rule 3:
+ * "no prompt or code content leaves the device" (telemetry and the trial
+ * email do leave — never claim "no data leaves the device").
+ */
+import { For } from "solid-js";
+import { createSignal, Show } from "solid-js";
+import { PageHead } from "~/components/PageHead";
+import { EdgeCacheHeaders } from "~/components/EdgeCacheHeaders";
+import SubdomainHeader from "~/components/SubdomainHeader";
+
+interface FaqEntry {
+ question: string;
+ answer: string;
+}
+
+const FAQ: FaqEntry[] = [
+ {
+ question: "Does The Nook read my prompts or my code?",
+ answer:
+ "No. The hooks and plugins that connect your agents to The Nook send session metadata only — which agent is running, which session, which event happened, which terminal it lives in. No prompt text and no code content leaves your device. Two things do leave the device, and we'd rather say so plainly: anonymous usage telemetry, and your email address if you choose to give it at trial start. That's the whole list."
+ },
+ {
+ question: "Do I need a Mac with a notch?",
+ answer:
+ "No. On a notched MacBook the island sits in the notch. On machines and displays without one, it anchors to the display your cursor is on, so a desktop setup with external monitors gets the same treatment. Everything inside the island — agent cards, fan control, the stats strip — works identically either way."
+ },
+ {
+ question: "How much battery does it cost?",
+ answer:
+ "The Nook is a small native UI, not a background agent — it watches the agents you're already running; it doesn't run anything itself. The heaviest thing it does is read and write your Mac's fan and charging controllers, which is a lightweight sensor read. If you're running enough coding agents to need it, those agents dwarf the app's own draw — which is rather the point: the fan control exists because the agents are what heat the machine."
+ },
+ {
+ question: "What permissions does it need?",
+ answer:
+ "The app itself runs unprivileged. Fan control and charge limiting go through a small helper component that macOS requires to run as root for hardware access — the app installs it on first use and you approve it once with your password. The hooks live in each agent's own config directory; you can inspect or remove every file it writes."
+ },
+ {
+ question: "Why macOS 14 or later?",
+ answer:
+ "The interface is built on Apple's current observation framework, which ships with macOS 14 (Sonoma). Supporting older versions would mean rewriting the whole UI layer on a deprecated foundation — time that goes into the app instead. macOS 14 runs on hardware from 2018 onward, which covers the machines that run coding agents comfortably."
+ },
+ {
+ question: "Does it work offline?",
+ answer:
+ "Yes. Everything The Nook does — the agent grid, permission cards, fan and thermal control — works with no network at all, and your agents' hooks talk to the app over a local socket. The network is only used for update checks, license activation, and the optional trial email."
+ }
+];
+
+export default function NookFaqPage() {
+ const [open, setOpen] = createSignal(0);
+
+ return (
+ <>
+
+
+
+
+
+ Questions people actually ask
+
+
+ The things that decide a purchase, answered without hedging. If
+ something here doesn't match your experience, that's a bug —{" "}
+
+ tell us
+
+ .
+