KOZMOS Hub: the launcher for the whole bundle. /kozmos opens a starfield sidebar listing every KOZMOS mod by where it draws, which ones are installed in this…

48 live, visual mods for Claude Code — in the terminal and in the desktop app.
Above the chat input, in the sidebar, in the transcript, on the status line and in your speakers: what Claude is doing right now, what it costs, how close you are to the 5-hour and weekly limits, what your subagents are up to, how full the context is, the state of your git tree and the heat of your machine.
<a href="https://www.youtube.com/watch?v=Eabw9kr9nVc"><img src="docs/media/kozmos-promo-poster.jpg" alt="KOZMOS: the 2:48 promo film on YouTube. Click to watch." width="860"></a>
<img src="docs/previews/orrery-aboveprompt-terminal.png" alt="orrery: subagents orbiting Claude above the prompt" width="860"> <table><tr> <td valign="top"><img src="docs/previews/bridge-pane-terminal.png" alt="bridge sidebar in the terminal" width="560"></td> <td valign="top"><picture><source media="(prefers-color-scheme: dark)" srcset="docs/previews/bridge-pane-desktop-dark.png"><img src="docs/previews/bridge-pane-desktop-light.png" alt="bridge sidebar in the desktop app" width="270"></picture></td> </tr></table>
KOZMOS is a bundle of independent function-hooks plugins ("mods"). Install one, a preset, or all of them. Every mod draws twice: for the terminal (colored text, cell rasters, 30 fps surface animations) and for the desktop app (animated SVG that follows the light and dark themes).
Requires Claude Code 2.1.290+, where function hooks are on by default. The mod API is early access and may change between releases.
/plugin marketplace add ersinkoc/claude-mods
/plugin install kozmos@kozmos
/plugin install bridge@kozmos
or one line per mod: /plugin install orrery --marketplace ersinkoc/claude-mods. Then type /kozmos: the hub lists every mod and opens each one with a click.
git clone https://github.com/ersinkoc/claude-mods.git
cd claude-mods
./install.ps1 # Windows: interactive menu
./install.ps1 -Preset showtime # essentials | showtime | all
./install.ps1 -Mods bridge,orrery,halo
./install.ps1 -Uninstall
./install.sh # macOS / Linux / Git Bash
./install.sh --preset all
The installer adds the folder as the kozmos marketplace and installs what you pick at the user scope, so the mods load in the CLI and in the desktop app's Code tab. A folder marketplace is read from the folder itself: after git pull, /reload-plugins picks up the new code. To try one mod for a single session: claude --plugin-dir mods/bridge.
Want way more than Claude Code? WrongStack is a free, open-source AI coding agent with a Brain, a Memory, and a full toolbox. It reads your code, edits files, runs commands, and coordinates specialist agents — across six surfaces, from a plain terminal REPL to a cross-machine HQ dashboard. No subscription required, and you keep your hand on every permission.
curl -fsSL https://wrongstack.com/install.sh | sh # macOS / Linux — self-contained binary
irm https://wrongstack.com/install.ps1 | iex # Windows (PowerShell) — no Node.js needed
| What you get | |
|---|---|
| 🌐 200+ LLM providers | Catalog pulled live from models.dev — Anthropic, OpenAI, Google, plus OAuth sign-in for Claude Pro/Max, ChatGPT and Copilot, and any OpenAI-compatible endpoint (Ollama, vLLM, LM Studio) |
| 🛠️ 67 built-in tools | Edits, lint/typecheck/test, execution, git, web, browser/E2E and a SQLite codebase index — every call gated by per-tool permissions |
| 🧠 SAGE memory | Project-wide long-term memory in SQLite/FTS5, anchored to files, symbols and commits — it gets better at your codebase over time |
| 🖥️ Six surfaces | Readline REPL · Ink/React TUI (--tui) · WebUI · SimpleUI · Desktop · cross-machine HQ (--hq) |
| 🤖 Fleet orchestration | A Director fans out specialist subagents over a project mailbox; eternal & parallel goal loops run until the contract verifies |
| 🔍 Chimera & Kanban | Auto-review agents that critique your diffs with severity-ranked findings, plus durable Kanban boards with atomic verification |
| 🔐 Secure by default | Encrypted secrets at rest, project-root containment, opt-in YOLO mode — MIT licensed, TypeScript-strict |
📊 The perfect pairing: KOZMOS's hourglass, bridge and throttle tell you exactly where your Claude limits stand — and WrongStack keeps you moving when they close in. It reads plan windows for Claude, ChatGPT, Copilot, Z.AI and more right in its own statusline and quota page, and when one provider runs dry, fallback chains rotate you onto the next model automatically. Watch the gauges, dodge the wall.
🔗 wrongstack.com · github.com/WrongStack/WrongStack · ⭐ Star it on GitHub
The pictures are the mods' own drawings, played through a made-up session in claude plugin test and rendered by scripts/render-previews.mjs.
<!-- gallery:start --> <!-- Generated by scripts/build-readme.mjs. Edit the mods' plugin.json descriptions instead. -->
One command opens them all.
/kozmosthe launcher for the whole bundle. /kozmos opens a starfield sidebar listing every KOZMOS mod by where it draws, which ones are installed in this session, and a button that runs each mod's own command.
<table><tr><td valign="top"><img src="docs/previews/kozmos-pane-terminal.png" alt="kozmos in the terminal" width="520"></td><td valign="top"><picture><source media="(prefers-color-scheme: dark)" srcset="docs/previews/kozmos-pane-desktop-dark.png"><img src="docs/previews/kozmos-pane-desktop-light.png" alt="kozmos in the desktop app" width="300"></picture></td></tr></table>
Docked beside the transcript; each toggles with its own command.
/almanacwhat this session is made of. Slash commands run, skills invoked, subagent types spawned, MCP servers used, models seen, plugins whose tools were called and tool families, each with counts and first-use time, as ranked bars and tag clouds; plus the engine version and how many installed commands went unused. /almanac toggles it.
<table><tr><td valign="top"><img src="docs/previews/almanac-pane-terminal.png" alt="almanac in the terminal" width="520"></td><td valign="top"><picture><source media="(prefers-color-scheme: dark)" srcset="docs/previews/almanac-pane-desktop-dark.png"><img src="docs/previews/almanac-pane-desktop-light.png" alt="almanac in the desktop app" width="300"></picture></td></tr></table>
/breadcrumbsthe session's web trail in a sidebar. WebSearch queries with result counts and top hits, WebFetch URLs with status and size, and browser-like MCP visits, grouped by domain with a monogram badge, clickable links and a copy-all-as-Markdown button. /breadcrumbs toggles it.
<table><tr><td valign="top"><img src="docs/previews/breadcrumbs-pane-terminal.png" alt="breadcrumbs in the terminal" width="520"></td><td valign="top"><picture><source media="(prefers-color-scheme: dark)" srcset="docs/previews/breadcrumbs-pane-desktop-dark.png"><img src="docs/previews/breadcrumbs-pane-desktop-light.png" alt="breadcrumbs in the desktop app" width="300"></picture></td></tr></table>
/bridgea mission-control sidebar. Model and effort, context fill, 5-hour and weekly limits with reset countdowns, session cost and burn rate, git branch and tree state, CPU/RAM/GPU, live subagents and the tool running now. /bridge toggles it.
<table><tr><td valign="top"><img src="docs/previews/bridge-pane-terminal.png" alt="bridge in the terminal" width="520"></td><td valign="top"><picture><source media="(prefers-color-scheme: dark)" srcset="docs/previews/bridge-pane-desktop-dark.png"><img src="docs/previews/bridge-pane-desktop-light.png" alt="bridge in the desktop app" width="300"></picture></td></tr></table>
/chroniclethe session as a timeline. Prompts, turn ends with duration, tool count and spend, subagents spawned and finished, commits and pushes, failed tools, compactions, and rate-limit and context milestones, each stamped HH:MM:SS, newest first, with filters. /chronicle toggles the sidebar.
<table><tr><td valign="top"><img src="docs/previews/chronicle-pane-terminal.png" alt="chronicle in the terminal" width="520"></td><td valign="top"><picture><source media="(prefers-color-scheme: dark)" srcset="docs/previews/chronicle-pane-desktop-dark.png"><img src="docs/previews/chronicle-pane-desktop-light.png" alt="chronicle in the desktop app" width="300"></picture></td></tr></table>
/faultlinethe error lens in a sidebar. Every failed or denied tool call, refused or errored turn and API error, grouped by a masked signature with count, first and last seen, the agents that hit it, the full first error (expandable) and a copy button, under a severity strip of the last 10 minutes. /faultline toggles it.
<table><tr><td valign="top"><img src="docs/previews/faultline-pane-terminal.png" alt="faultline in the terminal" width="520"></td><td valign="top"><picture><source media="(prefers-color-scheme: dark)" srcset="docs/previews/faultline-pane-desktop-dark.png"><img src="docs/previews/faultline-pane-desktop-light.png" alt="faultline in the desktop app" width="300"></picture></td></tr></table>
/gearboxtool analytics for the session. Per tool its calls, errors and error rate, average, p95 and max duration and total time as a sorted bar chart, the five slowest calls, calls per minute, and the main loop against subagents. /gearbox toggles the sidebar.
<table><tr><td valign="top"><img src="docs/previews/gearbox-pane-terminal.png" alt="gearbox in the terminal" width="520"></td><td valign="top"><picture><source media="(prefers-color-scheme: dark)" srcset="docs/previews/gearbox-pane-desktop-dark.png"><img src="docs/previews/gearbox-pane-desktop-light.png" alt="gearbox in the desktop app" width="300"></picture></td></tr></table>
/gitscopea git radar sidebar. Branch, upstream and ahead/behind, staged/changed/new/conflict counts and stashes, the last 8 commits on a graph rail, per-file +/- histograms from git diff, and a marker on every file this session edited. /gitscope toggles it.
<table><tr><td valign="top"><img src="docs/previews/gitscope-pane-terminal.png" alt="gitscope in the terminal" width="520"></td><td valign="top"><picture><source media="(prefers-color-scheme: dark)" srcset="docs/previews/gitscope-pane-desktop-dark.png"><img src="docs/previews/gitscope-pane-desktop-light.png" alt="gitscope in the desktop app" width="300"></picture></td></tr></table>
/hivemindthe agent swarm as a live tree. The main loop at the root and every subagent beneath its parent, each with a status glyph, type and task, model and effort, elapsed time, requests, tokens, estimated spend, context fill and the tool it runs now. /hivemind toggles the sidebar.
<table><tr><td valign="top"><img src="docs/previews/hivemind-pane-terminal.png" alt="hivemind in the terminal" width="520"></td><td valign="top"><picture><source media="(prefers-color-scheme: dark)" srcset="docs/previews/hivemind-pane-desktop-dark.png"><img src="docs/previews/hivemind-pane-desktop-light.png" alt="hivemind in the desktop app" width="300"></picture></td></tr></table>
/hourglassthe limit oracle. For the 5-hour, weekly and spend windows: percent used, reset countdown, a burn rate learned across sessions, the time to 100 % and a verdict on whether the reset comes first. Toasts at 80 % and 95 %, an optional status line. /hourglass toggles the sidebar.
<table><tr><td valign="top"><img src="docs/previews/hourglass-pane-terminal.png" alt="hourglass in the terminal" width="520"></td><td valign="top"><picture><source media="(prefers-color-scheme: dark)" srcset="docs/previews/hourglass-pane-desktop-dark.png"><img src="docs/previews/hourglass-pane-desktop-light.png" alt="hourglass in the desktop app" width="300"></picture></td></tr></table>
/launchpada pane of big launch buttons. It learns your most-used slash commands as you run them, keeps pinned favorites and saved prompts, and runs a command or fills the prompt box in one press. /launchpad toggles it; save <text>, rm <n>, pin <cmd>, unpin <cmd>, list.
<table><tr><td valign="top"><img src="docs/previews/launchpad-pane-terminal.png" alt="launchpad in the terminal" width="520"></td><td valign="top"><picture><source media="(prefers-color-scheme: dark)" srcset="docs/previews/launchpad-pane-desktop-dark.png"><img src="docs/previews/launchpad-pane-desktop-light.png" alt="launchpad in the desktop app" width="300"></picture></td></tr></table>
/mosaica GitHub-style activity calendar across sessions. 26 weeks of days colored by turns, tool calls, tokens or dollars, with the current and longest streak, the best day, totals and today's line. /mosaic toggles the sidebar.
<table><tr><td valign="top"><img src="docs/previews/mosaic-pane-terminal.png" alt="mosaic in the terminal" width="520"></td><td valign="top"><picture><source media="(prefers-color-scheme: dark)" srcset="docs/previews/mosaic-pane-desktop-dark.png"><img src="docs/previews/mosaic-pane-desktop-light.png" alt="mosaic in the desktop app" width="300"></picture></td></tr></table>
/relaythe API latency lab in a sidebar. Every model request of the main loop and its subagents with latency, time to first token, model, effort, input/output/cache tokens, tokens per second and stop reason; a latency histogram, tokens/sec over time, model mix, p50/p95, the slowest five and cache-read share per request. /relay toggles it.
<table><tr><td valign="top"><img src="docs/previews/relay-pane-terminal.png" alt="relay in the terminal" width="520"></td><td valign="top"><picture><source media="(prefers-color-scheme: dark)" srcset="docs/previews/relay-pane-desktop-dark.png"><img src="docs/previews/relay-pane-desktop-light.png" alt="relay in the desktop app" width="300"></picture></td></tr></table>
/spectrathe context X-ray. The context window broken down by category as a heat grid with a ranked legend, context growth per turn, and an estimate of how many turns remain before auto-compact. /spectra toggles the sidebar.
<table><tr><td valign="top"><img src="docs/previews/spectra-pane-terminal.png" alt="spectra in the terminal" width="520"></td><td valign="top"><picture><source media="(prefers-color-scheme: dark)" srcset="docs/previews/spectra-pane-desktop-dark.png"><img src="docs/previews/spectra-pane-desktop-light.png" alt="spectra in the desktop app" width="300"></picture></td></tr></table>
/switchboardan MCP observatory sidebar. Every MCP server in the session with its tools, calls, errors, average and p95 latency, last call and a status lamp (healthy, erroring, idle, busy); servers with no calls yet too, and per-tool rows on expand. On the desktop a patch bay with glowing cables to the active servers. /switchboard toggles it.
<table><tr><td valign="top"><img src="docs/previews/switchboard-pane-terminal.png" alt="switchboard in the terminal" width="520"></td><td valign="top"><picture><source media="(prefers-color-scheme: dark)" srcset="docs/previews/switchboard-pane-desktop-dark.png"><img src="docs/previews/switchboard-pane-desktop-light.png" alt="switchboard in the desktop app" width="300"></picture></td></tr></table>
/taskforgea live kanban of the session's tasks. TodoWrite lists and TaskCreate/TaskUpdate tasks fall into Pending, In progress and Done columns with the active form, time in the column and the owning agent, under a progress bar. /taskforge toggles the sidebar.
<table><tr><td valign="top"><img src="docs/previews/taskforge-pane-terminal.png" alt="taskforge in the terminal" width="520"></td><td valign="top"><picture><source media="(prefers-color-scheme: dark)" srcset="docs/previews/taskforge-pane-desktop-dark.png"><img src="docs/previews/taskforge-pane-desktop-light.png" alt="taskforge in the desktop app" width="300"></picture></td></tr></table>
/terminusthe session's shell history in a sidebar. Every Bash and PowerShell call of the main loop and its subagents with command, description, cwd, exit status, duration, output size and who ran it; filters for failed and slow runs, copy and re-run buttons. /terminus toggles it.
<table><tr><td valign="top"><img src="docs/previews/terminus-pane-terminal.png" alt="terminus in the terminal" width="520"></td><td valign="top"><picture><source media="(prefers-color-scheme: dark)" srcset="docs/previews/terminus-pane-desktop-dark.png"><img src="docs/previews/terminus-pane-desktop-light.png" alt="terminus in the desktop app" width="300"></picture></td></tr></table>
/thermala heatmap of the files this session touches. Reads, edits, writes and searches per file, a heat score that cools over time, grouped as a directory tree under the session folder: an indented heat tree on the terminal, a squarified treemap on the desktop, switchable by heat, reads or edits. /thermal toggles it.
<table><tr><td valign="top"><img src="docs/previews/thermal-pane-terminal.png" alt="thermal in the terminal" width="520"></td><td valign="top"><picture><source media="(prefers-color-scheme: dark)" srcset="docs/previews/thermal-pane-desktop-dark.png"><img src="docs/previews/thermal-pane-desktop-light.png" alt="thermal in the desktop app" width="300"></picture></td></tr></table>
/tokenomicsthe cost lab. Session cost as the headline, an estimated split by model and by token type, cache hit ratio, burn per hour, cost per turn, a pace forecast and daily and weekly totals across sessions with a 14-day chart. /tokenomics toggles the sidebar.
<table><tr><td valign="top"><img src="docs/previews/tokenomics-pane-terminal.png" alt="tokenomics in the terminal" width="520"></td><td valign="top"><picture><source media="(prefers-color-scheme: dark)" srcset="docs/previews/tokenomics-pane-desktop-dark.png"><img src="docs/previews/tokenomics-pane-desktop-light.png" alt="tokenomics in the desktop app" width="300"></picture></td></tr></table>
/vitalsa btop-style machine monitor sidebar. CPU with a braille history graph, RAM bar and history, disk use, NVIDIA GPU load, memory and temperature, process count, and the Claude side: session time, tool calls, running agents. Samples every 2 s only while open. /vitals toggles it.
<table><tr><td valign="top"><img src="docs/previews/vitals-pane-terminal.png" alt="vitals in the terminal" width="520"></td><td valign="top"><picture><source media="(prefers-color-scheme: dark)" srcset="docs/previews/vitals-pane-desktop-dark.png"><img src="docs/previews/vitals-pane-desktop-light.png" alt="vitals in the desktop app" width="300"></picture></td></tr></table>
Live drawings just above the chat input. Every band has a ✕ and its command brings it back.
/auroranorthern lights above the prompt while the model thinks. Shifting curtains whose intensity and speed follow the effort level (low is gentle, max is wild), a calmer single wave while it responds, and a label like 'thinking · xhigh · 12s'. /aurora toggles it.
<img src="docs/previews/aurora-aboveprompt-terminal.png" alt="aurora in the terminal" width="820">
/blackboxa flight recorder above the prompt. A live Gantt of the current turn: the main loop and each subagent on its own lane, every tool call a bar in its family color, model requests as ticks, a moving now cursor on an auto-scaling time axis; the last turn stays dimmed until the next. /blackbox toggles it.
<img src="docs/previews/blackbox-aboveprompt-terminal.png" alt="blackbox in the terminal" width="820">
<img src="docs/previews/blackbox-aboveprompt-desktop.svg" alt="blackbox in the desktop app, animated" width="820">
/glyphfallmatrix rain of tool activity above the prompt while Claude works. Every tool call drops its name and detail as a wave of letters, colored by tool family, with bright heads and fading tails, over a sparse rain of katakana; the last five tools sit below as colored chips. /glyphfall toggles it.
<img src="docs/previews/glyphfall-aboveprompt-terminal.png" alt="glyphfall in the terminal" width="820">
<img src="docs/previews/glyphfall-aboveprompt-desktop.svg" alt="glyphfall in the desktop app, animated" width="820">
/haloa one-row mood line above the prompt whose color and motion tell what Claude is doing: calm blue breathing when idle, a violet flow while it requests and writes, a magenta shimmer while it thinks, the running tool's color racing across, a red flash when a tool fails, an amber blink when it waits for you. /halo toggles it.
<img src="docs/previews/halo-aboveprompt-terminal.png" alt="halo in the terminal" width="820">
<img src="docs/previews/halo-aboveprompt-desktop.svg" alt="halo in the desktop app, animated" width="820">
/heartlinea neon EKG of the session drawn above the prompt. Every tool call is a spike colored by its family, every model request a blip, a failed tool a red inverted spike; the trace drifts green to red with context fill, with a live bpm (tool calls per minute) and ctx readout. /heartline toggles it.
<img src="docs/previews/heartline-aboveprompt-terminal.png" alt="heartline in the terminal" width="820">
<img src="docs/previews/heartline-aboveprompt-desktop.svg" alt="heartline in the desktop app, animated" width="820">
/orrerysubagents as pla
hooks/register.tsx 207 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { KozmosHub } from '../types'
5import { CATALOG } from './catalog.ts'
6import type { CatalogEntry } from './catalog.ts'
7import { KZ, fitText, hue, noise, pxOf, svg, svgText, xml } from './lib/kz.ts'
8
9const PANE = 'kz-hub'
10const TITLE = 'KOZMOS'
11const hubAtom = atom({ plugin: 'kozmos', key: 'hub' } as const, { installed: [], filter: 'all' })
12
13const CATEGORIES: readonly { id: string; label: string; color: string; where: string }[] = [
14 { id: 'all', label: 'All', color: KZ.violet, where: '' },
15 { id: 'pane', label: 'Sidebars', color: KZ.cyan, where: 'sidebar' },
16 { id: 'band', label: 'Bands', color: KZ.magenta, where: 'above the prompt' },
17 { id: 'transcript', label: 'Transcript', color: KZ.blue, where: 'in the transcript' },
18 { id: 'companion', label: 'Companions', color: KZ.amber, where: 'above the prompt' },
19 { id: 'status', label: 'Status', color: KZ.green, where: 'status line' },
20 { id: 'sound', label: 'Sound', color: KZ.yellow, where: 'speakers' },
21 { id: 'guard', label: 'Guards', color: KZ.red, where: 'before risky moves' },
22 { id: 'tool', label: 'Tools', color: KZ.teal, where: 'on demand' },
23]
24
25const colorOf = (category: string): string => CATEGORIES.find(c => c.id === category)?.color ?? KZ.mist
26
27async function refresh($: EngineInterface): Promise<void> {
28 try {
29 const names = (await $.command.list()).map(c => c.name)
30 await update($, hubAtom, prev => ({ ...prev, installed: CATALOG.filter(m => names.includes(m.name)).map(m => m.name) }))
31 } catch {
32 // Keep what the hub knew.
33 }
34}
35
36async function toggle($: EngineInterface): Promise<boolean> {
37 if ((await $.ui.panes()).some(p => p.id === PANE)) {
38 await $.ui.close({ id: PANE })
39 return false
40 }
41 await refresh($)
42 await $.ui.open({ id: PANE, title: TITLE })
43 return true
44}
45
46async function launch($: EngineInterface, name: string): Promise<void> {
47 try {
48 const { text } = await $.command.run({ command: name, args: '' })
49 await update($, hubAtom, prev => ({ ...prev, lastRun: { name, text: text ?? 'done' } }))
50 } catch (err) {
51 // The engine rejects a command run with an Error (no hook answered, a hook failed).
52 await update($, hubAtom, prev => ({ ...prev, lastRun: { name, text: (err as Error).message } }))
53 }
54}
55
56function listText(installed: readonly string[]): string {
57 // Only the categories the catalog has mods in get a heading.
58 const lines = CATEGORIES.filter(c => c.id !== 'all' && CATALOG.some(m => m.category === c.id)).flatMap(c => [
59 `${c.label}:`,
60 ...CATALOG.filter(m => m.category === c.id).map(m => ` ${installed.includes(m.name) ? '✓' : '·'} /${m.name} — ${m.blurb}`),
61 ])
62 return [`KOZMOS — ${installed.length}/${CATALOG.length} mods active`, ...lines].join('\n')
63}
64
65export const register: Register = on => {
66 on('session.start', async ($, e, next) => {
67 const started = await next(e)
68 await $.command.register({ name: 'kozmos', description: 'KOZMOS: open the hub of every KOZMOS mod (/kozmos list prints them)', argumentHint: '[list]', immediate: true })
69 await refresh($)
70 return started
71 })
72
73 on('command.run', { command: 'kozmos' }, async ($, e) => {
74 if (e.args.trim() === 'list') {
75 await refresh($)
76 return { text: listText((await read($, hubAtom)).installed) }
77 }
78 return { text: (await toggle($)) ? 'KOZMOS hub open.' : 'KOZMOS hub closed.' }
79 })
80
81 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
82 const hub: KozmosHub = await read($, hubAtom)
83 const ui = $.ui.resolve(e)
84 const { Box, Text, Button } = ui
85 const shown = CATALOG.filter(m => hub.filter === 'all' || m.category === hub.filter)
86 const filters = (
87 <Box key="filters" flexDirection="row" flexWrap="wrap" gap={1}>
88 {CATEGORIES.map(c => (
89 <Button
90 key={`f-${c.id}`}
91 label={`${hub.filter === c.id ? '●' : '○'} ${c.label}`}
92 plain
93 onPress={() => update($, hubAtom, prev => ({ ...prev, filter: c.id }))}
94 />
95 ))}
96 </Box>
97 )
98 const footer = hub.lastRun ? (
99 <Text key="last" dimColor wrap="truncate-end">
100 /{hub.lastRun.name}: {hub.lastRun.text}
101 </Text>
102 ) : null
103
104 if ('Svg' in ui && e.surface !== 'terminal') {
105 const { Svg } = ui
106 const W = pxOf(e.props.bodyColumns, 44)
107 return (
108 <Box flexDirection="column" gap={1}>
109 <Svg source={banner(W, hub.installed.length, CATALOG.length)} alt={`KOZMOS, ${hub.installed.length} of ${CATALOG.length} mods active`} width={W} height={92} />
110 {filters}
111 {shown.map(m => (
112 <Box key={`row-${m.name}`} flexDirection="row" alignItems="center" gap={1}>
113 <Svg source={modCard(W - 76, m, hub.installed.includes(m.name))} alt={`${m.title}: ${m.blurb}`} width={W - 76} height={50} />
114 <Button key={`run-${m.name}`} label="Open" onPress={() => void launch($, m.name)} />
115 </Box>
116 ))}
117 {footer}
118 </Box>
119 )
120 }
121
122 const cols = Math.max(30, e.props.bodyColumns || 40)
123 const word = 'K O Z M O S'
124 return (
125 <Box flexDirection="column">
126 <Text bold>
127 {[...word].map((ch, i) => (
128 <Text key={`w${i}`} color={hue(0.72 + i / 28)}>
129 {ch}
130 </Text>
131 ))}
132 <Text dimColor> {hub.installed.length}/{CATALOG.length} active</Text>
133 </Text>
134 <Text dimColor wrap="truncate-end">{starLine(cols)}</Text>
135 {filters}
136 {shown.map(m => {
137 const isOn = hub.installed.includes(m.name)
138 return (
139 <Box key={`row-${m.name}`} flexDirection="column" marginTop={1}>
140 <Box flexDirection="row" justifyContent="space-between">
141 <Text wrap="truncate-end">
142 <Text color={isOn ? colorOf(m.category) : KZ.mist}>{isOn ? '◆' : '◇'} </Text>
143 <Text bold={isOn} dimColor={!isOn}>{m.title}</Text>
144 <Text dimColor> · /{m.name}</Text>
145 </Text>
146 {isOn ? <Button key={`run-${m.name}`} label="open" plain onPress={() => void launch($, m.name)} /> : <Text dimColor>not installed</Text>}
147 </Box>
148 <Text dimColor wrap="truncate-end"> {m.blurb}</Text>
149 </Box>
150 )
151 })}
152 {shown.length === 0 && <Text dimColor>Nothing here yet.</Text>}
153 {footer}
154 </Box>
155 )
156 })
157}
158
159/** A row of drifting stars for the terminal header. */
160function starLine(cols: number): string {
161 let s = ''
162 for (let i = 0; i < cols; i++) {
163 const n = noise(i, 7)
164 s += n > 0.93 ? '✦' : n > 0.85 ? '·' : n > 0.8 ? '˙' : ' '
165 }
166 return s
167}
168
169/** The desktop banner: a starfield with twinkling stars and a gradient wordmark. */
170function banner(W: number, active: number, total: number): string {
171 const stars: string[] = []
172 for (let i = 0; i < 70; i++) {
173 const x = noise(i, 1) * W
174 const y = noise(i, 2) * 92
175 const r = 0.4 + noise(i, 3) * 1.3
176 const d = (noise(i, 4) * 3).toFixed(2)
177 stars.push(`<circle cx="${x.toFixed(1)}" cy="${y.toFixed(1)}" r="${r.toFixed(2)}" fill="#fff" class="tw" style="animation-delay:-${d}s"/>`)
178 }
179 const css = `.tw{animation:tw 3s ease-in-out infinite}@keyframes tw{50%{opacity:.15}}
180.orb{transform-box:fill-box;transform-origin:center;animation:orb 14s linear infinite}@keyframes orb{to{transform:rotate(360deg)}}`
181 const body = `<defs>
182<linearGradient id="sky" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#140c2e"/><stop offset=".55" stop-color="#1d1147"/><stop offset="1" stop-color="#0b2a3a"/></linearGradient>
183<linearGradient id="word" x1="0" y1="0" x2="1" y2="0"><stop offset="0" stop-color="${KZ.violet}"/><stop offset=".5" stop-color="${KZ.magenta}"/><stop offset="1" stop-color="${KZ.cyan}"/></linearGradient>
184<radialGradient id="neb" cx=".8" cy=".3" r=".6"><stop offset="0" stop-color="${KZ.magenta}" stop-opacity=".35"/><stop offset="1" stop-color="${KZ.magenta}" stop-opacity="0"/></radialGradient>
185</defs>
186<rect width="${W}" height="92" rx="14" fill="url(#sky)"/>
187<rect width="${W}" height="92" rx="14" fill="url(#neb)"/>
188${stars.join('')}
189<g transform="translate(${W - 58},46)"><g class="orb"><ellipse rx="30" ry="9" fill="none" stroke="${KZ.cyan}" stroke-opacity=".5"/><circle cx="30" cy="0" r="3" fill="${KZ.cyan}"/></g><circle r="9" fill="${KZ.amber}"/></g>
190<text x="18" y="50" font-family="'Segoe UI',Inter,sans-serif" font-size="30" font-weight="800" letter-spacing="7" fill="url(#word)">KOZMOS</text>
191<text x="20" y="72" font-family="'Segoe UI',Inter,sans-serif" font-size="11.5" fill="#c9c3ef">${xml(`${active} of ${total} mods active in this session`)}</text>`
192 return svg(W, 92, body, css)
193}
194
195/** One mod's desktop card; exported so a test can draw an entry the catalog does not hold yet. */
196export function modCard(W: number, m: CatalogEntry, isOn: boolean): string {
197 const c = colorOf(m.category)
198 const where = CATEGORIES.find(x => x.id === m.category)?.where ?? ''
199 const body = `<rect class="p" width="${W}" height="50" rx="10"/>
200<rect x="0" y="0" width="4" height="50" rx="2" fill="${c}" opacity="${isOn ? 1 : 0.35}"/>
201<circle cx="20" cy="17" r="5" fill="${isOn ? c : 'none'}" stroke="${c}" stroke-width="1.5" class="${isOn ? 'pulse' : ''}"/>
202${svgText(32, 21, m.title, { size: 13.5, weight: 650 })}
203${svgText(W - 10, 21, isOn ? `/${m.name}` : 'not installed', { cls: 'm', size: 10.5, anchor: 'end', mono: true })}
204${svgText(14, 40, fitText(`${where ? where + ' · ' : ''}${m.blurb}`, 11, W - 24), { cls: 's', size: 11 })}`
205 return svg(W, 50, body)
206}
207hooks/catalog.ts 55 lines1// GENERATED by scripts/build-catalog.mjs from every mod's plugin.json. Do not edit.
2
3export type Category = 'pane' | 'band' | 'transcript' | 'companion' | 'status' | 'sound' | 'guard' | 'tool' | 'extra'
4export type CatalogEntry = { name: string; title: string; category: Category; blurb: string }
5
6export const CATALOG: readonly CatalogEntry[] = [
7 { name: "almanac", title: "Almanac", category: "pane", blurb: "what this session is made of." },
8 { name: "breadcrumbs", title: "Breadcrumbs", category: "pane", blurb: "the session's web trail in a sidebar." },
9 { name: "bridge", title: "Bridge", category: "pane", blurb: "a mission-control sidebar." },
10 { name: "chronicle", title: "Chronicle", category: "pane", blurb: "the session as a timeline." },
11 { name: "faultline", title: "Faultline", category: "pane", blurb: "the error lens in a sidebar." },
12 { name: "gearbox", title: "Gearbox", category: "pane", blurb: "tool analytics for the session." },
13 { name: "gitscope", title: "Gitscope", category: "pane", blurb: "a git radar sidebar." },
14 { name: "hivemind", title: "Hivemind", category: "pane", blurb: "the agent swarm as a live tree." },
15 { name: "hourglass", title: "Hourglass", category: "pane", blurb: "the limit oracle." },
16 { name: "launchpad", title: "Launchpad", category: "pane", blurb: "a pane of big launch buttons." },
17 { name: "mosaic", title: "Mosaic", category: "pane", blurb: "a GitHub-style activity calendar across sessions." },
18 { name: "relay", title: "Relay", category: "pane", blurb: "the API latency lab in a sidebar." },
19 { name: "spectra", title: "Spectra", category: "pane", blurb: "the context X-ray." },
20 { name: "switchboard", title: "Switchboard", category: "pane", blurb: "an MCP observatory sidebar." },
21 { name: "taskforge", title: "Taskforge", category: "pane", blurb: "a live kanban of the session's tasks." },
22 { name: "terminus", title: "Terminus", category: "pane", blurb: "the session's shell history in a sidebar." },
23 { name: "thermal", title: "Thermal", category: "pane", blurb: "a heatmap of the files this session touches." },
24 { name: "tokenomics", title: "Tokenomics", category: "pane", blurb: "the cost lab." },
25 { name: "vitals", title: "Vitals", category: "pane", blurb: "a btop-style machine monitor sidebar." },
26 { name: "aurora", title: "Aurora", category: "band", blurb: "northern lights above the prompt while the model thinks." },
27 { name: "blackbox", title: "Blackbox", category: "band", blurb: "a flight recorder above the prompt." },
28 { name: "glyphfall", title: "Glyphfall", category: "band", blurb: "matrix rain of tool activity above the prompt while Claude works." },
29 { name: "halo", title: "Halo", category: "band", blurb: "a one-row mood line above the prompt whose color and motion tell what Claude is doing: calm blue breathing when idle, a violet flow while it requests and writes, a magenta shimmer while it thinks, the running tool's color racing across, a red flash when a tool fails, an amber blink when it waits for you." },
30 { name: "heartline", title: "Heartline", category: "band", blurb: "a neon EKG of the session drawn above the prompt." },
31 { name: "orrery", title: "Orrery", category: "band", blurb: "subagents as planets circling a central sun above the prompt." },
32 { name: "questline", title: "Questline", category: "band", blurb: "task progress above the prompt." },
33 { name: "skies", title: "Skies", category: "band", blurb: "the session's weather above the prompt." },
34 { name: "storyboard", title: "Storyboard", category: "band", blurb: "Claude narrates its own progress." },
35 { name: "throttle", title: "Throttle", category: "band", blurb: "a car dashboard above the prompt." },
36 { name: "tidewater", title: "Tidewater", category: "band", blurb: "the live diff of the current turn as tides above the prompt." },
37 { name: "verdict", title: "Verdict", category: "band", blurb: "test, build and lint results above the prompt." },
38 { name: "warpdrive", title: "Warpdrive", category: "band", blurb: "a starfield at warp above the prompt." },
39 { name: "compass", title: "Compass", category: "transcript", blurb: "the single most useful hint under the prompt." },
40 { name: "mantra", title: "Mantra", category: "transcript", blurb: "a themed spinner." },
41 { name: "stamp", title: "Stamp", category: "transcript", blurb: "a receipt on every finished turn." },
42 { name: "toolmarks", title: "Toolmarks", category: "transcript", blurb: "badges on tool rows." },
43 { name: "clawdling", title: "Clawdling", category: "companion", blurb: "a pixel crab companion that lives above the prompt." },
44 { name: "epilogue", title: "Epilogue", category: "companion", blurb: "a recap band after every turn." },
45 { name: "laurels", title: "Laurels", category: "companion", blurb: "32 achievements earned from what the session does (Hydra, Night Owl, Cache Wizard, Bug Squasher, Polyglot, Centurion, Committed and more), kept across sessions with their unlock dates." },
46 { name: "tempo", title: "Tempo", category: "companion", blurb: "a Pomodoro focus timer above the prompt." },
47 { name: "marquee", title: "Marquee", category: "status", blurb: "a status-line ticker." },
48 { name: "lofi", title: "Lofi", category: "sound", blurb: "generative lo-fi music while Claude works." },
49 { name: "resonance", title: "Resonance", category: "sound", blurb: "a synthesized soundscape for the session." },
50 { name: "ballast", title: "Ballast", category: "guard", blurb: "a context coach." },
51 { name: "thrift", title: "Thrift", category: "guard", blurb: "a frugal mode." },
52 { name: "warden", title: "Warden", category: "guard", blurb: "a guard for destructive shell commands." },
53 { name: "polaroid", title: "Polaroid", category: "tool", blurb: "/polaroid snapshots the session as a self-contained dark-neon HTML report in .kozmos/reports: model, duration, cost, tokens, context peak, rate limits, tool calls by family, a timeline of turns, subagents, files touched and commands run, all collected live." },
54]
55hooks/lib/kz.ts 510 lines1// GENERATED by scripts/sync-shared.mjs from shared/kz.ts. Do not edit here.
2// KOZMOS shared kit. Source of truth: shared/kz.ts at the bundle root.
3// `node scripts/sync-shared.mjs` copies it into every mod as hooks/lib/kz.ts,
4// because a hooks module may only import files inside its own plugin.
5// Edit the root copy, never a mod's copy.
6
7// ---------------------------------------------------------------------------
8// Palette: one neon family across every KOZMOS mod.
9
10export const KZ = {
11 violet: '#a78bfa',
12 magenta: '#f472b6',
13 cyan: '#22d3ee',
14 teal: '#2dd4bf',
15 green: '#4ade80',
16 lime: '#a3e635',
17 yellow: '#facc15',
18 amber: '#fb923c',
19 red: '#f87171',
20 blue: '#60a5fa',
21 clay: '#d97757',
22 ink: '#1f1e1d',
23 mist: '#9ca3af',
24} as const
25
26/** Tool families, colored the same in every mod. */
27export function toolColor(tool: string): string {
28 const t = String(tool)
29 if (t === 'Bash' || t === 'PowerShell') return KZ.green
30 if (t === 'Edit' || t === 'Write' || t === 'NotebookEdit' || t === 'MultiEdit') return KZ.yellow
31 if (t === 'Read' || t === 'Glob' || t === 'Grep' || t === 'LSP') return KZ.blue
32 if (t === 'Agent' || t === 'Task' || t === 'Workflow') return KZ.violet
33 if (t.startsWith('Web')) return KZ.cyan
34 if (t.startsWith('Todo') || t.startsWith('Task')) return KZ.teal
35 if (t.startsWith('mcp__')) return KZ.magenta
36 return KZ.mist
37}
38
39/** A short glyph per tool family. */
40export function toolGlyph(tool: string): string {
41 const t = String(tool)
42 if (t === 'Bash' || t === 'PowerShell') return '$'
43 if (t === 'Edit' || t === 'Write' || t === 'NotebookEdit') return '✎'
44 if (t === 'Read') return '◉'
45 if (t === 'Glob' || t === 'Grep') return '⌕'
46 if (t === 'Agent') return '◈'
47 if (t.startsWith('Web')) return '◍'
48 if (t.startsWith('Todo') || t.startsWith('Task')) return '☑'
49 if (t.startsWith('mcp__')) return '⬡'
50 return '•'
51}
52
53// ---------------------------------------------------------------------------
54// Colors.
55
56export function hexToRgb(hex: string): [number, number, number] {
57 const h = hex.replace('#', '')
58 const n = parseInt(h.length === 3 ? h.split('').map(c => c + c).join('') : h, 16)
59 return [(n >> 16) & 255, (n >> 8) & 255, n & 255]
60}
61
62export function rgbToHex(r: number, g: number, b: number): string {
63 const c = (v: number) => Math.max(0, Math.min(255, Math.round(v))).toString(16).padStart(2, '0')
64 return `#${c(r)}${c(g)}${c(b)}`
65}
66
67export function mix(a: string, b: string, t: number): string {
68 const [r1, g1, b1] = hexToRgb(a)
69 const [r2, g2, b2] = hexToRgb(b)
70 const k = clamp01(t)
71 return rgbToHex(r1 + (r2 - r1) * k, g1 + (g2 - g1) * k, b1 + (b2 - b1) * k)
72}
73
74/** Green at 0, yellow at 0.6, red at 1: the heat of a gauge. */
75export function heat(t: number): string {
76 const k = clamp01(t)
77 return k < 0.6 ? mix(KZ.green, KZ.yellow, k / 0.6) : mix(KZ.yellow, KZ.red, (k - 0.6) / 0.4)
78}
79
80/** A smooth rainbow for hues 0..1 (for auroras, plasma and the like). */
81export function hue(h: number, s = 0.75, l = 0.6): string {
82 const k = ((h % 1) + 1) % 1
83 const a = s * Math.min(l, 1 - l)
84 const f = (n: number) => {
85 const x = (n + k * 12) % 12
86 return l - a * Math.max(-1, Math.min(x - 3, 9 - x, 1))
87 }
88 return rgbToHex(f(0) * 255, f(8) * 255, f(4) * 255)
89}
90
91export function hexToInt(hex: string): number {
92 const [r, g, b] = hexToRgb(hex)
93 return (r << 16) | (g << 8) | b
94}
95
96// ---------------------------------------------------------------------------
97// Numbers and text.
98
99export const clamp01 = (v: number): number => (Number.isFinite(v) ? Math.max(0, Math.min(1, v)) : 0)
100
101export function fmtTokens(n: number): string {
102 if (!Number.isFinite(n)) return '—'
103 // Each unit starts where the one below would round up to 1000 of itself.
104 if (n >= 999.5e6) return `${(n / 1e9).toFixed(1)}B`
105 if (n >= 999_500) return `${(n / 1e6).toFixed(n >= 1e7 ? 0 : 1)}M`
106 if (n >= 999.5) return `${(n / 1e3).toFixed(n >= 1e4 ? 0 : 1)}k`
107 return `${Math.round(n)}`
108}
109
110export function fmtUsd(usd: number): string {
111 if (!Number.isFinite(usd)) return '$—'
112 if (usd < 0.01 && usd > 0) return '<$0.01'
113 return usd < 100 ? `$${usd.toFixed(2)}` : `$${Math.round(usd)}`
114}
115
116/** 0:42, 3:07, 1:02:33. */
117export function fmtClock(ms: number): string {
118 const s = Math.max(0, Math.round(ms / 1000))
119 const h = Math.floor(s / 3600)
120 const m = Math.floor((s % 3600) / 60)
121 const ss = String(s % 60).padStart(2, '0')
122 return h ? `${h}:${String(m).padStart(2, '0')}:${ss}` : `${m}:${ss}`
123}
124
125/** 42s, 7m, 2h41m, 3d4h: compact durations for countdowns. */
126export function fmtSpan(ms: number): string {
127 const s = Math.max(0, Math.round(ms / 1000))
128 if (s < 60) return `${s}s`
129 const m = Math.floor(s / 60)
130 if (m < 60) return `${m}m`
131 const h = Math.floor(m / 60)
132 if (h < 48) return `${h}h${String(m % 60).padStart(2, '0')}m`
133 return `${Math.floor(h / 24)}d${h % 24}h`
134}
135
136export function fmtPct(p: number | undefined): string {
137 return p === undefined || !Number.isFinite(p) ? '—' : `${Math.round(p)}%`
138}
139
140export function clip(s: string, max: number): string {
141 const one = s.replace(/\s+/g, ' ').trim()
142 return one.length > max ? one.slice(0, Math.max(1, max - 1)) + '…' : one
143}
144
145export function padEnd(s: string, n: number): string {
146 return s.length >= n ? s.slice(0, n) : s + ' '.repeat(n - s.length)
147}
148
149export function padStart(s: string, n: number): string {
150 return s.length >= n ? s.slice(s.length - n) : ' '.repeat(n - s.length) + s
151}
152
153/** `claude-opus-5-5[1m]` → `Opus 5.5`. */
154export function modelName(id: string | undefined): string {
155 if (!id) return '—'
156 const m = /(fable|mythos|opus|sonnet|haiku)-(\d+)(?:-(\d{1,2})(?!\d))?/i.exec(id)
157 if (!m) return id.replace(/^claude-/, '').replace(/\[.*\]$/, '')
158 const fam = m[1]!
159 return `${fam.charAt(0).toUpperCase()}${fam.slice(1).toLowerCase()} ${m[2]}${m[3] ? '.' + m[3] : ''}`
160}
161
162/** Last path segment, either slash. */
163export function baseName(p: string): string {
164 const parts = p.split(/[\\/]/).filter(Boolean)
165 return parts[parts.length - 1] ?? p
166}
167
168/** A stable small hash for seeding. */
169export function hash(s: string): number {
170 let h = 2166136261
171 for (let i = 0; i < s.length; i++) {
172 h ^= s.charCodeAt(i)
173 h = Math.imul(h, 16777619)
174 }
175 return h >>> 0
176}
177
178/** Deterministic 0..1 noise from two ints. */
179export function noise(x: number, y: number): number {
180 const s = Math.sin(x * 12.9898 + y * 78.233) * 43758.5453
181 return s - Math.floor(s)
182}
183
184/** A seeded PRNG (mulberry32). */
185export function rng(seed: number): () => number {
186 let a = seed >>> 0
187 return () => {
188 a = (a + 0x6d2b79f5) >>> 0
189 let t = a
190 t = Math.imul(t ^ (t >>> 15), t | 1)
191 t ^= t + Math.imul(t ^ (t >>> 7), t | 61)
192 return ((t ^ (t >>> 14)) >>> 0) / 4294967296
193 }
194}
195
196// ---------------------------------------------------------------------------
197// Text gauges for the terminal.
198
199const EIGHTHS = ['', '▏', '▎', '▍', '▌', '▋', '▊', '▉']
200
201/** A smooth bar with eighth blocks: `█████▍ `. */
202export function bar(ratio: number, width: number, empty = '░'): string {
203 const w = Math.max(1, Math.floor(width))
204 const exact = clamp01(ratio) * w
205 const full = Math.floor(exact)
206 const part = EIGHTHS[Math.floor((exact - full) * 8)] ?? ''
207 const used = full + (part ? 1 : 0)
208 return '█'.repeat(full) + part + empty.repeat(Math.max(0, w - used))
209}
210
211/** A segmented gauge: `▰▰▰▱▱▱`. */
212export function pips(ratio: number, width: number, on = '▰', off = '▱'): string {
213 const w = Math.max(1, Math.floor(width))
214 const n = Math.round(clamp01(ratio) * w)
215 return on.repeat(n) + off.repeat(w - n)
216}
217
218const SPARK = '▁▂▃▄▅▆▇█'
219
220/** A sparkline of the last `width` values, scaled to their own max (or `max`). */
221export function sparkline(values: readonly number[], width: number, max?: number): string {
222 const tail = values.slice(-Math.max(1, width))
223 const top = max ?? Math.max(1e-9, ...tail)
224 const line = tail.map(v => SPARK[Math.min(7, Math.max(0, Math.round((v / top) * 7)))] ?? '▁').join('')
225 return line.padStart(width, ' ')
226}
227
228/** Braille line graph, 2 samples per cell, `rows` cells tall. */
229export function brailleGraph(values: readonly number[], width: number, rows: number, max?: number): string[] {
230 const samples = values.slice(-(width * 2))
231 const top = max ?? Math.max(1e-9, ...samples)
232 const dotsTall = rows * 4
233 const grid: number[][] = Array.from({ length: rows }, () => Array.from({ length: width }, () => 0))
234 const offset = width * 2 - samples.length
235 // Dot bits per column, from the bottom row of a cell up.
236 const LEFT = [0x40, 0x04, 0x02, 0x01]
237 const RIGHT = [0x80, 0x20, 0x10, 0x08]
238 samples.forEach((v, i) => {
239 const x = offset + i
240 const col = Math.floor(x / 2)
241 const isRight = x % 2 === 1
242 const h = Math.round(clamp01(v / top) * (dotsTall - 1))
243 for (let d = 0; d <= h; d++) {
244 const row = rows - 1 - Math.floor(d / 4)
245 const bits = isRight ? RIGHT : LEFT
246 const cellRow = grid[row]
247 if (cellRow && col >= 0) cellRow[col] = (cellRow[col] ?? 0) | bits[d % 4]!
248 }
249 })
250 return grid.map(r => r.map(b => String.fromCharCode(0x2800 + b)).join(''))
251}
252
253// ---------------------------------------------------------------------------
254// Raster: a grid of colored cells, packed as the engine wants it.
255
256const B64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
257
258export function toBase64(bytes: Uint8Array): string {
259 let out = ''
260 let i = 0
261 for (; i + 2 < bytes.length; i += 3) {
262 const n = (bytes[i]! << 16) | (bytes[i + 1]! << 8) | bytes[i + 2]!
263 out += B64[(n >> 18) & 63]! + B64[(n >> 12) & 63]! + B64[(n >> 6) & 63]! + B64[n & 63]!
264 }
265 const rest = bytes.length - i
266 if (rest === 1) {
267 const n = bytes[i]! << 16
268 out += B64[(n >> 18) & 63]! + B64[(n >> 12) & 63]! + '=='
269 } else if (rest === 2) {
270 const n = (bytes[i]! << 16) | (bytes[i + 1]! << 8)
271 out += B64[(n >> 18) & 63]! + B64[(n >> 12) & 63]! + B64[(n >> 6) & 63]! + '='
272 }
273 return out
274}
275
276/** The terminal's own default color, for a cell's foreground or background. */
277export const DEFAULT_COLOR = 0x01000000
278
279export class Canvas {
280 readonly cols: number
281 readonly rows: number
282 private readonly words: Uint32Array
283
284 constructor(cols: number, rows: number) {
285 this.cols = Math.max(1, Math.min(512, Math.floor(cols)))
286 this.rows = Math.max(1, Math.min(256, Math.floor(rows)))
287 this.words = new Uint32Array(this.cols * this.rows * 3)
288 this.clear()
289 }
290
291 clear(bg: number = DEFAULT_COLOR): void {
292 for (let i = 0; i < this.cols * this.rows; i++) {
293 this.words[i * 3] = 0x20
294 this.words[i * 3 + 1] = DEFAULT_COLOR
295 this.words[i * 3 + 2] = bg
296 }
297 }
298
299 /** Puts one width-1 character; colors are '#rrggbb' or a packed int. */
300 set(x: number, y: number, ch: string, fg?: string | number, bg?: string | number): void {
301 const cx = Math.floor(x)
302 const cy = Math.floor(y)
303 if (cx < 0 || cy < 0 || cx >= this.cols || cy >= this.rows) return
304 const i = (cy * this.cols + cx) * 3
305 const code = ch.codePointAt(0) ?? 0x20
306 this.words[i] = code > 0xffff || code < 0x20 ? 0x20 : code
307 if (fg !== undefined) this.words[i + 1] = typeof fg === 'number' ? fg : hexToInt(fg)
308 if (bg !== undefined) this.words[i + 2] = typeof bg === 'number' ? bg : hexToInt(bg)
309 }
310
311 /** Writes a string left to right, clipped to the canvas. */
312 text(x: number, y: number, s: string, fg?: string | number, bg?: string | number): void {
313 let cx = x
314 for (const ch of s) {
315 this.set(cx, y, ch, fg, bg)
316 cx++
317 }
318 }
319
320 /** Paints a cell's background only, keeping its character. */
321 paint(x: number, y: number, bg: string | number): void {
322 const cx = Math.floor(x)
323 const cy = Math.floor(y)
324 if (cx < 0 || cy < 0 || cx >= this.cols || cy >= this.rows) return
325 this.words[(cy * this.cols + cx) * 3 + 2] = typeof bg === 'number' ? bg : hexToInt(bg)
326 }
327
328 /** Two vertical pixels per cell with the upper-half block: `py` is in half-cells. */
329 pixel(x: number, py: number, color: string): void {
330 const cx = Math.floor(x)
331 const cy = Math.floor(py / 2)
332 if (cx < 0 || cy < 0 || cx >= this.cols || cy >= this.rows) return
333 const i = (cy * this.cols + cx) * 3
334 const c = hexToInt(color)
335 const isTop = Math.floor(py) % 2 === 0
336 if (this.words[i] !== 0x2580) {
337 this.words[i] = 0x2580
338 this.words[i + 1] = DEFAULT_COLOR
339 this.words[i + 2] = DEFAULT_COLOR
340 }
341 if (isTop) this.words[i + 1] = c
342 else this.words[i + 2] = c
343 }
344
345 /** The `cells` prop of a Raster. */
346 encode(): string {
347 return toBase64(new Uint8Array(this.words.buffer))
348 }
349}
350
351// ---------------------------------------------------------------------------
352// SVG for the desktop: one drawing per row (the desktop wraps siblings).
353
354export const FONT = "-apple-system,BlinkMacSystemFont,'Segoe UI',Inter,sans-serif"
355export const MONO = "ui-monospace,'Cascadia Code','SF Mono',Consolas,monospace"
356
357const XML_ESC: Record<string, string> = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }
358
359export function xml(s: string): string {
360 return s.replace(/[&<>"']/g, c => XML_ESC[c]!)
361}
362
363/** Theme-aware classes every KOZMOS drawing shares: t text, s secondary, m muted, k track, p panel. */
364export const SVG_BASE_CSS = `
365.t{fill:#1f1f1f}.s{fill:#5f5f5c}.m{fill:#8e8e8a}.k{fill:#e7e5e0}.p{fill:#f5f4f1}.ln{stroke:#e1dfda}
366@media (prefers-color-scheme: dark){.t{fill:#ededed}.s{fill:#b4b4b0}.m{fill:#7c7c78}.k{fill:#2d2d2b}.p{fill:#232322}.ln{stroke:#363634}}
367.pulse{animation:kzp 1.6s ease-in-out infinite}@keyframes kzp{50%{opacity:.35}}
368.spin{transform-box:fill-box;transform-origin:center;animation:kzs 2s linear infinite}@keyframes kzs{to{transform:rotate(360deg)}}
369@media (prefers-reduced-motion: reduce){*{animation:none!important}}
370`
371
372export function svg(width: number, height: number, body: string, css = ''): string {
373 return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}"><style>${SVG_BASE_CSS}${css}</style>${body}</svg>`
374}
375
376/** Rough width of UI text in px, for fitting labels. */
377export function textWidth(s: string, size: number): number {
378 let w = 0
379 for (const ch of s) w += (/[\s.,:;'|!il1()[\]]/.test(ch) ? 0.3 : /[A-Z@%MWmw]/.test(ch) ? 0.72 : 0.56) * size
380 return w
381}
382
383export function fitText(s: string, size: number, maxW: number): string {
384 if (textWidth(s, size) <= maxW) return s
385 let out = ''
386 for (const ch of s) {
387 if (textWidth(out + ch + '…', size) > maxW) break
388 out += ch
389 }
390 return out + '…'
391}
392
393/** A rounded progress bar as SVG markup. */
394export function svgBar(x: number, y: number, w: number, h: number, ratio: number, color: string): string {
395 const fw = Math.max(0, Math.min(w, w * clamp01(ratio)))
396 return `<rect class="k" x="${x}" y="${y}" width="${w}" height="${h}" rx="${h / 2}"/>` +
397 (fw > 0 ? `<rect x="${x}" y="${y}" width="${Math.max(h, fw)}" height="${h}" rx="${h / 2}" fill="${color}"/>` : '')
398}
399
400/** SVG text helper. */
401export function svgText(x: number, y: number, s: string, opts: { cls?: string; size?: number; weight?: number; anchor?: 'start' | 'middle' | 'end'; fill?: string; mono?: boolean } = {}): string {
402 const { cls = 't', size = 12, weight = 400, anchor = 'start', fill, mono = false } = opts
403 return `<text ${fill ? `fill="${fill}"` : `class="${cls}"`} x="${x}" y="${y}" font-family="${mono ? MONO : FONT}" font-size="${size}" font-weight="${weight}" text-anchor="${anchor}" font-variant-numeric="tabular-nums">${xml(s)}</text>`
404}
405
406/** Pixel width a desktop pane or band gives a drawing for `columns` reported columns. */
407export function pxOf(columns: number | undefined, fallback = 60, slack = 8): number {
408 return Math.max(200, Math.min(1600, (columns || fallback) * 8 - slack))
409}
410
411// ---------------------------------------------------------------------------
412// Prices, USD per million tokens: input, output, cache read, cache write (5 min).
413// Anthropic first-party rates as of 2026-10. Used only where the engine reports
414// tokens and not money (a subagent's own spend); the session total comes from
415// $.session.usage().cost, which is the engine's.
416
417const PRICES: [RegExp, [number, number, number, number]][] = [
418 [/fable|mythos/i, [10, 50, 0.25, 12.5]],
419 [/opus-5-5/i, [4, 20, 0.2, 5]],
420 [/opus/i, [5, 25, 0.5, 6.25]],
421 [/sonnet-5/i, [2, 10, 0.2, 2.5]],
422 [/sonnet/i, [3, 15, 0.3, 3.75]],
423 [/haiku-5/i, [0.1, 0.5, 0.01, 0.125]],
424 [/haiku/i, [1, 5, 0.1, 1.25]],
425]
426
427export type Usage = {
428 input_tokens?: number
429 output_tokens?: number
430 cache_read_input_tokens?: number
431 cache_creation_input_tokens?: number
432}
433
434export function priceOf(model: string): [number, number, number, number] {
435 return PRICES.find(([re]) => re.test(model))?.[1] ?? [4, 20, 0.2, 5]
436}
437
438export function costOf(model: string, u: Usage | null | undefined): number {
439 if (!u) return 0
440 const [i, o, r, w] = priceOf(model)
441 return ((u.input_tokens ?? 0) * i + (u.output_tokens ?? 0) * o + (u.cache_read_input_tokens ?? 0) * r + (u.cache_creation_input_tokens ?? 0) * w) / 1e6
442}
443
444export function tokensOf(u: Usage | null | undefined): number {
445 if (!u) return 0
446 return (u.input_tokens ?? 0) + (u.output_tokens ?? 0) + (u.cache_read_input_tokens ?? 0) + (u.cache_creation_input_tokens ?? 0)
447}
448
449/** The context a request filled: everything it read plus what it wrote. */
450export function contextOf(u: Usage | null | undefined): number {
451 return tokensOf(u)
452}
453
454export function windowOf(model: string): number {
455 return /haiku-4/i.test(model) ? 200_000 : 1_000_000
456}
457
458// ---------------------------------------------------------------------------
459// Rate limits.
460
461export type Limit = { kind: string; percentUsed: number; resetsAt?: string }
462
463export function limitLabel(kind: string): string {
464 if (kind === 'five_hour') return '5h'
465 if (kind === 'seven_day') return '7d'
466 if (kind === 'spend_limit') return '$'
467 return kind.replace(/_/g, ' ')
468}
469
470/** Milliseconds until a window resets, or undefined. */
471export function untilReset(l: Limit, now: number): number | undefined {
472 if (!l.resetsAt) return undefined
473 const t = Date.parse(l.resetsAt)
474 return Number.isFinite(t) ? Math.max(0, t - now) : undefined
475}
476
477/** The length of a window in ms, for burn-rate math. */
478export function windowMs(kind: string): number | undefined {
479 if (kind === 'five_hour') return 5 * 3600_000
480 if (kind === 'seven_day') return 7 * 24 * 3600_000
481 return undefined
482}
483
484// ---------------------------------------------------------------------------
485// Tool calls, described in a few words.
486
487/** What a tool call is about: the command, the file, the pattern, the agent's task. */
488export function toolDetail(tool: string, input: unknown): string {
489 const e = (input ?? {}) as Record<string, unknown>
490 const s = (k: string) => (typeof e[k] === 'string' ? (e[k] as string) : '')
491 const t = String(tool)
492 if (t === 'Bash' || t === 'PowerShell') return clip(s('description') || s('command'), 60)
493 if (s('file_path')) return baseName(s('file_path'))
494 if (s('notebook_path')) return baseName(s('notebook_path'))
495 if (t === 'Agent') return clip(s('description') || s('subagent_type'), 60)
496 if (s('pattern')) return clip(s('pattern'), 60)
497 if (s('url')) return clip(s('url').replace(/^https?:\/\//, ''), 60)
498 if (s('query')) return clip(s('query'), 60)
499 if (s('subject')) return clip(s('subject'), 60)
500 if (Array.isArray(e.todos)) return `${(e.todos as unknown[]).length} todos`
501 if (t.startsWith('mcp__')) return t.split('__').slice(1).join(' · ')
502 return ''
503}
504
505/** `mcp__server__tool` → `server·tool`; built-ins unchanged. */
506export function toolName(tool: string): string {
507 const t = String(tool)
508 return t.startsWith('mcp__') ? t.split('__').slice(1).join('·') : t
509}
510types/index.d.ts 15 lines1export type KozmosHub = {
2 /** Commands this session can run, refreshed when the hub opens. */
3 installed: string[]
4 /** The category the hub shows, or 'all'. */
5 filter: string
6 /** The last mod whose command the hub ran, and what it answered. */
7 lastRun?: { name: string; text: string }
8}
9
10declare module 'claude-code' {
11 interface PluginState {
12 kozmos: { hub: KozmosHub }
13 }
14}
15