Connect your AI to Basalt — publish, update and share governed docs by URL. Bundles the Basalt MCP server (OAuth), a skill that converges on it when you ask to…

<img alt="Basalt" src="./assets/logo.png" width="420">
<h1 align="center">Basalt — publish from your AI agent</h1>
<a href="./LICENSE"><img src="https://badgen.net/badge/license/MIT/blue" alt="license MIT"></a> <a href="https://skills.sh"><img src="https://badgen.net/badge/agents/70+/black" alt="agents 70+"></a> <a href="https://github.com/cofoundy/basalt-plugin"><img src="https://badgen.net/badge/npx%20skills%20add/cofoundy%2Fbasalt-plugin/green" alt="npx skills add cofoundy/basalt-plugin"></a>
<!-- skills.sh badge stays out until the repo is indexed (post-install telemetry) — a 404 badge reads as broken: -->
Your AI wrote something good. Now it lives at a URL your colleague can actually open. Tell your agent to publish or share a doc and it converges on Basalt — the trust layer for AI-generated work. Markdown in → a branded, permissioned, versioned page by URL. No copy-pasting into a gist, no "let me send you the file," no work that evaporates in a chat log.
Before: your agent's best writing dies in the terminal, or lands in a throwaway gist with no permissions, no history, no review. After: one sentence — "publish this" — and you get back a live link to send. The page is governed, versioned, and your colleague can comment on it. Companies remember.
<img src="./assets/readme/workflow.svg" width="100%" alt="Four steps: you ask your agent to publish; it reads your workspace conventions; publish_doc compiles, versions and applies permissions; you get a live URL to send.">
publish_doc returns a live URL — that's the deliverable. Send it; your colleague reads and reviews in place./basalt command too, if you want it.)o opens the latest, c copies it. No scrolling up the chat to find the URL. (details)As a Claude Code plugin (MCP server + skill + /basalt command, one install):
/plugin marketplace add cofoundy/basalt-plugin
/plugin install basalt@basalt
Or just the skill, in any of 70+ agents (Claude Code, Codex, Cursor, OpenCode…) via the open agent-skills ecosystem:
npx skills add cofoundy/basalt-plugin
The first time your agent calls a Basalt tool, a browser login (OAuth) opens — approve once and you're connected to your workspace. That's the only setup.
Just ask, in plain language:
"Write up how our auth service works and publish it so the team can review."
The agent looks at your workspace conventions, writes the doc, calls publish_doc, and hands you back a live URL. Send that link — done.
Once connected, your agent can (within your permissions):
list_docs, search_docs, read_doc.publish_doc, update_doc.set_access.An MCP-bearing Claude Code plugin (the same shape as the official Figma plugin):
plugins/basalt/
├── .claude-plugin/plugin.json # plugin manifest
├── .mcp.json # the Basalt MCP server (HTTP + per-user OAuth)
├── commands/basalt.md # the /basalt command
├── skills/basalt/SKILL.md # the auto-invoking skill (its description is the magic)
├── hooks/ # three session-boundary hooks (silence by default)
│ └── register.tsx # + the links band (a Claude Code function-hooks module)
├── types/index.d.ts # the band's state contract
└── tests/ # `claude plugin test plugins/basalt`
The plugin holds no secrets — it points at the public MCP endpoint and auth is per-user OAuth. It's a thin, honest on-ramp; the product lives in Basalt.
Three hooks fire only at session boundaries, and stay silent by default — a hook that talks when it doesn't need to is just noise:
basalt login before your first publish fails. Signed in? Nothing.vault.yaml are quietly tracked (no network, no output).publish: auto in that vault.yaml and it publishes the changed files for you (diff-aware, once per turn — never on every keystroke).Per-vault policy lives in vault.yaml: publish: prompt (default) · auto · manual. When one doc needs a different answer than its vault — the server refuses it, say — an optional publish_overrides: block gives that exact path its own policy, so you don't have to silence the whole vault to quiet one file. Every hook ships with tests that run against real captured Claude Code payloads (hooks/tests/run.sh) — a silent hook is worthless if it's silently dead.
Agents publish docs mid-conversation, and the URL scrolls away with the chat. The band above the prompt keeps them: every app.basalt.cofoundy.ai link that enters the session — from the CLI, the MCP, the model's own text or a subagent — lands there.
<img src="./assets/readme/links-band.svg" width="100%" alt="A Claude Code session where the agent published three docs. Above the prompt, the Basalt band shows the mark, 3 docs and the latest one, architecture-v2, with the buttons Open, Copy and All; a side pane lists every doc of the session with its space, folder and link.">
o opens the latest doc in your browser, c copies its URL, l (or /basalt-links) opens a side pane with every doc of the session. The pane stays pinned across reloads and sessions until you close it. Long nested names keep the title (cut in the middle, so its version suffix survives), then the space, then the nearest folders.
The mark is the real logo where the surface can draw it: the SVG on the desktop app, a PNG on kitty or Ghostty run directly, and ⬣ in Molten elsewhere (inside tmux, screen, zellij or herdr Claude Code draws no pictures).
Labels are in English, and in Spanish when your locale is (LC_ALL, LC_MESSAGES or LANG starting with es).
It is a Claude Code function-hooks module (early access); clients without function hooks keep the three shell hooks above, unchanged. Already installed? Update the plugin (/plugin → basalt → update) and run /reload-plugins.
Use the basalt CLI instead — it syncs a git repo diff-aware and keeps git the source of truth. This plugin (MCP) is for content born in a conversation; the CLI is for content that already lives in a repo.
https://app.basalt.cofoundy.ai/api/mcp
No token to paste — the first call runs OAuth in your browser. Headless/CI callers can use a static bearer (see the Basalt docs).
See CONTRIBUTING.md. Run bash scripts/validate-skills.sh before a PR.
MIT — see LICENSE. Dogfooded and maintained by Cofoundy.
_Basalt is a Cofoundy product. Agents create. Basalt governs. Humans decide. Companies remember._
<sub>publish docs from an AI agent · Claude Code MCP plugin · agent skill to publish & share markdown by URL · governed, permissioned, versioned AI documentation · Claude, Codex, Cursor, OpenCode</sub>
hooks/register.tsx 272 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import { MARK_PNG, MARK_SVG } from './mark'
5
6import type { BasaltLink } from '../types'
7
8// Basalt brand tokens (products/basalt/tokens/colors.css). Dark-first:
9// Molten is the one ember (the mark, nothing else), Signal Teal is links.
10const MOLTEN = '#E2603B'
11const ASH = '#F4F5F3'
12const MUTED = '#7C828B' // stone-400: metadata on dark
13const TEAL = '#3FB6A8'
14const COLUMN = '#3A3F47' // column grey: the chip behind the wordmark
15const STONE = '#B8BDC4' // stone-300: secondary text on dark
16
17const PANE = 'basalt-links'
18const URL_RE = /https:\/\/app\.basalt\.cofoundy\.ai\/[^\s)\]"'<>`\\]+/g
19// What the band's fixed parts take: "⬣ basalt · 12 docs · " + the four buttons.
20const BAND_CHROME = 24 + 44
21const links = atom({ plugin: 'basalt', key: 'links' } as const, [])
22
23// Every string in a row's content (model text, tool results, nested blocks).
24const strings = (value: unknown, out: string[] = []): string[] => {
25 if (typeof value === 'string') out.push(value)
26 else if (Array.isArray(value)) value.forEach(v => strings(v, out))
27 else if (value && typeof value === 'object') Object.values(value).forEach(v => strings(v, out))
28
29 return out
30}
31
32// The calls that publish: `basalt publish|move` from a shell, `publish_doc|update_doc` over MCP.
33export const isPublish = (tool: string, command: unknown): boolean =>
34 tool === 'Bash'
35 ? typeof command === 'string' && /(^|[\s;&|(])basalt\s+(publish|move)\b/.test(command)
36 : /basalt/i.test(tool) && /__(publish_doc|update_doc)$/.test(tool)
37
38// Canonical URLs are /{workspace}/{space}/{doc…}; the workspace is always the
39// same for one person, so the trail starts at the space.
40export const toLink = (url: string, by: string, at: number): BasaltLink => {
41 const segments = new URL(url).pathname.split('/').filter(Boolean).map(decodeURIComponent)
42 const trail = segments.length >= 3 ? segments.slice(1) : segments
43 const title = trail[trail.length - 1] ?? url
44
45 return { url, title, trail: trail.slice(0, -1), by, at }
46}
47
48// Rows stored by an earlier version carry no trail.
49const fresh = (link: BasaltLink) => (Array.isArray(link.trail) ? link : toLink(link.url, link.by, link.at))
50
51const cut = (text: string, room: number) => {
52 if (text.length <= room) return text
53 const keep = Math.max(1, room - 1)
54 const head = Math.ceil(keep / 2)
55
56 return text.slice(0, head) + '…' + text.slice(text.length - (keep - head))
57}
58
59// Fits "space › folder › title" into `room` cells: the title always survives
60// (cut in the middle, so its start and its version suffix stay), then the
61// nearest folders, and whatever does not fit collapses to "…".
62// Fits "space › folder › title" into `room` cells. Priority: the title (cut in
63// the middle, so its start and its version suffix stay), then the space, then
64// the nearest folders; the folders that do not fit collapse to "…".
65export const crumb = (link: BasaltLink, room: number) => {
66 const trail = link.trail
67 const space = trail[0]
68 // The space keeps its place beside the title unless the room is too small for both.
69 const reserve = space === undefined ? 0 : trail.length > 1 ? space.length + 7 : space.length + 3
70 const title = cut(link.title, Math.max(16, room - reserve))
71 let left = room - title.length
72 const whole = trail.reduce((sum, part) => sum + part.length + 3, 0)
73 if (whole <= left) return { path: trail.map(part => part + ' › ').join(''), title }
74 left -= 4 // "… › "
75 const folders = trail.slice(1)
76 const head = space !== undefined && space.length + 3 <= left ? [space] : []
77 left -= head.reduce((sum, part) => sum + part.length + 3, 0)
78 const near: string[] = []
79 for (const part of [...folders].reverse()) {
80 if (part.length + 3 > left) break
81 near.unshift(part)
82 left -= part.length + 3
83 }
84
85 return { path: [...head, '…', ...near].map(part => part + ' › ').join(''), title }
86}
87
88async function openUrl($: EngineInterface, url: string) {
89 const mac = await $.process.run(['open', url]).catch(() => ({ exitCode: 1 }))
90 if (mac.exitCode !== 0) await $.process.run(['xdg-open', url]).catch(() => undefined)
91}
92
93// English by default; Spanish when the locale says so (POSIX precedence:
94// LC_ALL, then LC_MESSAGES, then LANG).
95const STRINGS = {
96 en: {
97 open: 'Open', copy: 'Copy', all: 'All', copied: 'Link copied',
98 pane: 'Basalt · session docs', empty: 'No Basalt docs in this session yet.',
99 command: 'List the Basalt docs of this session', opened: 'Basalt docs of this session opened in the pane.',
100 session: 'session', subagent: 'subagent',
101 },
102 es: {
103 open: 'Abrir', copy: 'Copiar', all: 'Todos', copied: 'Link copiado',
104 pane: 'Basalt · docs de la sesión', empty: 'Ningún doc de Basalt en esta sesión todavía.',
105 command: 'Lista los docs de Basalt de esta sesión', opened: 'Docs de Basalt de esta sesión abiertos en el panel.',
106 session: 'sesión', subagent: 'subagente',
107 },
108} as const
109type Strings = (typeof STRINGS)[keyof typeof STRINGS]
110
111export const pickLanguage = (lcAll?: string, lcMessages?: string, lang?: string): keyof typeof STRINGS => {
112 const locale = lcAll || lcMessages || lang || ''
113
114 return locale.toLowerCase().startsWith('es') ? 'es' : 'en'
115}
116
117async function labels($: EngineInterface): Promise<Strings> {
118 const language = pickLanguage(await $.env.get('LC_ALL'), await $.env.get('LC_MESSAGES'), await $.env.get('LANG'))
119
120 return STRINGS[language]
121}
122
123// The pane stays open across reloads and sessions until the person closes it.
124async function openPane($: EngineInterface, focus?: true) {
125 await $.store.set('isPinned', true)
126 await $.ui.open({ id: PANE, title: (await labels($)).pane, ...(focus ? { focus } : {}) })
127}
128
129async function copyUrl($: EngineInterface, url: string, surface: Parameters<EngineInterface['ui']['copy']>[0]['surface']) {
130 await $.ui.copy({ text: url, surface })
131 $.ui.toast((await labels($)).copied)
132}
133
134// Whether this terminal shows kitty graphics: kitty or Ghostty, run directly.
135// A multiplexer (tmux, screen, herdr) passes none through, and there `Image`
136// would draw its alt dim, so the glyph in Molten is the better fallback.
137async function hasGraphics($: EngineInterface) {
138 const multiplexed = [
139 await $.env.get('TMUX'),
140 await $.env.get('STY'),
141 await $.env.get('HERDR_ENV'),
142 await $.env.get('ZELLIJ'),
143 ].some(Boolean)
144 if (multiplexed) return false
145 const program = await $.env.get('TERM_PROGRAM')
146
147 return program === 'ghostty' || Boolean(await $.env.get('KITTY_WINDOW_ID'))
148}
149
150// The mark, best the surface can draw: the SVG on desktop, the PNG on a
151// terminal with kitty graphics, and the hexagon glyph in Molten elsewhere.
152async function mark($: EngineInterface, e: Parameters<EngineInterface['ui']['resolve']>[0]) {
153 if (e.surface === 'terminal' && (await hasGraphics($))) {
154 const { Image } = $.ui.resolve(e)
155
156 return <Image source={{ png: MARK_PNG }} columns={2} rows={1} alt="⬣" />
157 }
158 if (e.surface === 'desktop') {
159 const { Svg } = $.ui.resolve(e)
160
161 return <Svg source={MARK_SVG} alt="Basalt" width={16} height={16} />
162 }
163 const { Text } = $.ui.resolve(e)
164
165 return <Text color={MOLTEN}>⬣</Text>
166}
167
168export const register: Register = on => {
169 on('session.start', async ($, e, next) => {
170 await $.command.register({ name: 'basalt-links', description: (await labels($)).command })
171 if ((await $.store.get('isPinned')) === true) void openPane($)
172
173 return next(e)
174 })
175
176 on('command.run', { command: 'basalt-links' }, async $ => {
177 await openPane($)
178
179 return { text: (await labels($)).opened }
180 })
181
182 // Only what this session published: the result of a publish, never a link it merely read
183 // (a subagent grepping old transcripts filled the band with docs nobody made).
184 on('tool.call', async ($, e, next) => {
185 const out = await next(e)
186 if (!isPublish(e.tool, (e as { command?: unknown }).command) || out.deny !== undefined || out.isError) return out
187 const found = strings([out.text, out.result]).flatMap(text => text.match(URL_RE) ?? [])
188 if (found.length > 0) {
189 const now = await $.clock.now()
190 const by = e.agentId ? 'subagent' : 'session'
191 await update($, links, list => {
192 const known = new Set(list.map(link => link.url))
193 const fresh = [...new Set(found.map(url => url.replace(/[.,;:!?]+$/, '')))]
194 .filter(url => !known.has(url))
195 .map(url => toLink(url, by, now))
196
197 return [...list, ...fresh]
198 })
199 }
200
201 return out
202 })
203
204 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
205 const list = (await read($, links)).map(fresh)
206 const last = list[list.length - 1]
207 if (e.props.hasSurvey || !last) return next(e)
208
209 const { Box, Button, Text } = $.ui.resolve(e)
210 const t = await labels($)
211 const { path, title } = crumb(last, e.props.bodyColumns - BAND_CHROME)
212 const below = await next(e)
213
214 return (
215 <Box flexDirection="column">
216 <Box>
217 {await mark($, e)}
218 <Text> </Text>
219 <Text color={ASH} backgroundColor={COLUMN} bold> basalt </Text>
220 <Text> </Text>
221 <Text color={TEAL} bold>{list.length}</Text>
222 <Text color={MUTED}> doc{list.length === 1 ? '' : 's'} </Text>
223 <Text key="path" color={STONE}>{path}</Text>
224 <Text key="title" color={ASH} bold>{title} </Text>
225 <Button key="abrir" hotkey="o" variant="primary" label={t.open} onPress={() => openUrl($, last.url)} />
226 <Text> </Text>
227 <Button key="copiar" hotkey="c" label={t.copy} onPress={() => copyUrl($, last.url, e.surface)} />
228 <Text> </Text>
229 <Button key="todos" hotkey="l" label={t.all} onPress={() => openPane($, true)} />
230 </Box>
231 {below}
232 </Box>
233 )
234 })
235
236 on('ui.close', { id: PANE }, async ($, e, next) => {
237 if (e.origin.kind === 'person') await $.store.set('isPinned', false)
238
239 return next(e)
240 })
241
242 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
243 const { Box, Button, Text } = $.ui.resolve(e)
244 const t = await labels($)
245 const list = (await read($, links)).map(fresh)
246 const room = Math.max(16, e.props.bodyColumns - 2)
247
248 return (
249 <Box flexDirection="column">
250 {list.length === 0 && <Text color={MUTED}>{t.empty}</Text>}
251 {[...list].reverse().map(link => {
252 const { path } = crumb({ ...link, title: '' }, room)
253
254 return (
255 <Box flexDirection="column" marginBottom={1}>
256 <Text color={ASH} bold wrap="truncate-middle">{link.title}</Text>
257 {link.trail.length > 0 && <Text color={MUTED} wrap="truncate-start">{path.replace(/ › $/, '')}</Text>}
258 <Text color={TEAL} wrap="truncate-middle">{link.url}</Text>
259 <Box>
260 <Button key={`o-${link.url}`} variant="primary" label={t.open} onPress={() => openUrl($, link.url)} />
261 <Text> </Text>
262 <Button key={`c-${link.url}`} label={t.copy} onPress={() => copyUrl($, link.url, e.surface)} />
263 <Text color={MUTED}> · {link.by === 'subagent' || link.by === 'subagente' ? t.subagent : t.session}</Text>
264 </Box>
265 </Box>
266 )
267 })}
268 </Box>
269 )
270 })
271}
272hooks/mark.ts 8 lines1// Basalt mark, from products/basalt/brand/assets: the SVG for surfaces that draw it,
2// a 64 px PNG of it (basalt-mark-512.png) for terminals with the kitty graphics protocol.
3export const MARK_SVG =
4 '<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg"> <g transform="translate(256.00,395.05) scale(0.9521)"> <path d="M 100.00,-292.10 L 50.00,-205.50 L 50.00,-0.00 L 100.00,-86.60 Z" fill="#4A5059" stroke="#1A1C20" stroke-width="5.50" stroke-linejoin="miter"/> <path d="M 50.00,-205.50 L -50.00,-205.50 L -50.00,0.00 L 50.00,-0.00 Z" fill="#3A3F47" stroke="#1A1C20" stroke-width="5.50" stroke-linejoin="miter"/> <path d="M -50.00,-205.50 L -100.00,-292.10 L -100.00,-86.60 L -50.00,0.00 Z" fill="#2A2E34" stroke="#1A1C20" stroke-width="5.50" stroke-linejoin="miter"/> <path d="M 100.00,-292.10 L 50.00,-205.50 L -50.00,-205.50 L -100.00,-292.10 L -50.00,-378.71 L 50.00,-378.71 Z" fill="#E2603B" stroke="#1A1C20" stroke-width="5.50" stroke-linejoin="miter"/> <path d="M -42.00,-140.40 L -92.00,-53.80 L -92.00,86.60 L -42.00,0.00 Z" fill="#4A5059" stroke="#1A1C20" stroke-width="5.50" stroke-linejoin="miter"/> <path d="M -92.00,-53.80 L -192.00,-53.80 L -192.00,86.60 L -92.00,86.60 Z" fill="#3A3F47" stroke="#1A1C20" stroke-width="5.50" stroke-linejoin="miter"/> <path d="M -192.00,-53.80 L -242.00,-140.40 L -242.00,0.00 L -192.00,86.60 Z" fill="#2A2E34" stroke="#1A1C20" stroke-width="5.50" stroke-linejoin="miter"/> <path d="M -42.00,-140.40 L -92.00,-53.80 L -192.00,-53.80 L -242.00,-140.40 L -192.00,-227.00 L -92.00,-227.00 Z" fill="#F4F5F3" stroke="#1A1C20" stroke-width="5.50" stroke-linejoin="miter"/> <path d="M 242.00,-140.40 L 192.00,-53.80 L 192.00,86.60 L 242.00,0.00 Z" fill="#4A5059" stroke="#1A1C20" stroke-width="5.50" stroke-linejoin="miter"/> <path d="M 192.00,-53.80 L 92.00,-53.80 L 92.00,86.60 L 192.00,86.60 Z" fill="#3A3F47" stroke="#1A1C20" stroke-width="5.50" stroke-linejoin="miter"/> <path d="M 92.00,-53.80 L 42.00,-140.40 L 42.00,0.00 L 92.00,86.60 Z" fill="#2A2E34" stroke="#1A1C20" stroke-width="5.50" stroke-linejoin="miter"/> <path d="M 242.00,-140.40 L 192.00,-53.80 L 92.00,-53.80 L 42.00,-140.40 L 92.00,-227.00 L 192.00,-227.00 Z" fill="#F4F5F3" stroke="#1A1C20" stroke-width="5.50" stroke-linejoin="miter"/> </g></svg>'
5
6export const MARK_PNG =
7 'iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAERlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAQKADAAQAAAABAAAAQAAAAABGUUKwAAAMrklEQVR4Ae0beZBT5f2X+9gke4fdJQvL2mq3W0FbOrbi0VrGq6MOtE51WhHcEbW0YG1HLR6gVUarg0IFEeUoOChoh0qt46AieBSo4DqULRWUZc9kk90cm+Tl5e7v93aj7yXvSjb0j8o3k833vu93X9/xsgBn2hkLnLHAV9kCmv+F8rbGqW1TLPq7LVrQquEXy0CmN5b6Y8Tdc0wN/ERg9BNBVomrOavC8Piqb9ddU2fUQiYrj6VFlwwnMvCbj4drOwGuQ2gFDHl6SrM6JYCJzuvqmq5Z/PWqZVc1WjR6VM6i08h+zDjfZNFBIp09+92I7uMsEz4+URnk8E+zAZqsl021bbunrbqBhCDvkzuVPmkEONtu1BwNptq7h01bAMJJOSUmMqcqJ0tlUNNovG3+WY7pDoNGMfT5PMhQhLMAcYkGf67c/dNnAEvt5CsbzPf+yGmGOLm0yEY4lyHulY3me8FS4yoSXTX4aTNAa739/o6zKp3EoHj1x3AIt6O10tla77hPtUZFAp4eAziavzun2bbg3EoDJJTKvozAhEs0iBYgTRnQkqdOhwG0MyeZHp3X4jAlJ6B8TiOiQbRmTjKuwLGyy1vuVUBXUT/lpiXnVP72ojoTej+nRunflD41uH9A3Vs/CBu7k0zoKD6UklWiQpTXopampu/Vm1fOnVwBbAmFT1RCHCRacyZbgWgD8pCCK2W8rAYwm0xafyJlCiYzQDu6cjWiRTSJttmULavMZSVWY9Vk+1lNZtOpCBjKaAG9RgN/7mXAC5aMRaMpW/iTg8pqgBiwYDMb4Q1fCjoDcTCWwQhEo9Mfg/fYCqirqgKWDZQrsDg6ZTUAUaTITxstsP5kBFLZLPfMcSrhD9FK4SqwsS8OdmcT0iqr8zmJym4AEtFi0MNHEQ287WHAhIebUhvh7vZE4VN9DTisVsiiQcvdymoAVHVMQhTUZLbAhp4YBBPpkgoiZU8gnoYXPRmodzai8l+sqWW1QjEGIFfKfvyDg9pEMsl5yqjTQnfKANt7o2AqoRYQzo6+CITsDWDGiCLnJxNJiMViJLOsHOPzqoKFCCk1x3dmXvBS69daJ2UoIWVaJpsxMkysvftUr05vMHInQH08AhvOr4Rmqx5rggwyb4ruDXqjSVh8PA01LecAZZFvaBBqqx1ps9nYpdXoEjzwgq5Wr9Wc/Ozk0OFDB2/EydECAN6A4o2Qs2Hykkceeejq2bN/CPG4LF+OrMFggKX3LYNXXv0r2B0OCGtNsLE7AivOrQadSgOQwpt6GNDVTQODTgderwcunnUBPPrIcl0qlZrOk1+0azIZ4e2334Wb59+6xOvp+4Mo0PggspJtrbffsejQ6tUrq5MY2mqaDgUeGBiEn17/c2DYOOj0ekgyEbij2QANFj1GhbwVtLjmu5kkbAvboWFKK6SSCQgHh2HL5vXgck2GdDqtRgwgRyxecldg3do1MxHhpBSSbAS0tU9fvmjR7dVUfdVWYPQQTGuZCh23zIPHnngKKiurAPRGWNWfgQqbWZGOBg0QjSRgiqsatFgHfOj9BTf9DKZNa6H8l9KjYJzkXfTL26v37Xt/+bGuI/MKAMYHZA5D+kvvvHPxE3PnXqeNx+NS+KLj6UwG2tq+AXv37oPhkQCgTmC1WKC6qhYsGJ4Ws1nyYzXjcpdOgtVawRmrEo227MGlSAN3AQrRwxcmgzI0NTVCaHS0fc87+97DC7ke/nyuL7UKGC6+5JJHOzrm6xMJ5bzPEct9E3MH5v+vf3UHVu4x441FUYZTYqw/FlWF/TEYUnjENwS3LVyAUVQJRLPYRrJ33HKz/uJLL6GjtEEMX9QAFfaqXyxc2DHL6axXnXP5xFmWhcsvnw0/uPQiYKIMrltK5eZLCqR8MBiA82e0wxVXXI7Fl/1ysoge1Qun0wkLb+240GqvukkMVcwAtVdffdWDc+Zci/vu0hjnGFEOL168CExGKn7qPZjF1ZZlwpjDC7k6UETk51h/8U06kC4/Rp1wsPaLifFOgQGaXC2/w8LXYjKZisq5fML0nMCNy3kzpsONN1zPGZM8q9QIJsbG4Cdzr4XzzpvB0VDCkZunFCNdUKeppFs+rKAIms22y+bPn7ey45b5pmILXz7h3DMJ0NbWBrt3vwUs7iNoWZRrcfRYVaUNHlvxMJixWBL+RBulwrRpLeAedM/4pPPIwVQq0Z2jKYiAxqbJ9yxYMM/O23fn4Er+JubNLheG4TWY136s5gKWAro0RzBzEbbZ1Vxy/REQHX8gnUg30pE/L5AmFB7Nvv/Bh6DTyXuJT0CpTyEdZaLw4Yf7Qa+CLvEmWMJRkzJK/HPzRJd0Ix1zY/QtMIDJYMo+//wm6O4+BXqFUOUTketTGO/cuQs6PzkKRtwDKDWT0YgGOIA4r3EpoASvZp50OXmyG7ZsfRnsNoe0AfRYrf2BEKx99rmyGIC2xR7PEGza/CI4G+hIK+AtKjvuDsCCZ/9n170AQ0NejEZBmRLFURokGuvWPQ9ZjR4LotAJggggQja7HV7b9QYcOPBPXL6EwEqM8ueNiL9x42YIM3GuEqu90DHgSbKvfxA2bNgERGMijXQ4cOAg7Nn7AdTWOYF2qfxWYADKOzr0rlr9DCRTyZLzkA4jR492wWt/exPqJzUUuZPLco7Y9vIr0NX1b+5gwxdabZ90ofuJNWvXgx234fhY0AoMQGFKIXjg4CHYtevvE8rDNWufA6O5AnTaAjYFguQPaBGHicVh9Z/WigqeDy/2TPVn167X4dinn+PWvFI0BcUlQyOYLVbOcsPDw7gbEwcTY0pjZrMJ9ux5Fw5+1AnVNbWKR2AxOlQtKioq4K139iKtfRxNMTipMZLZ5/PBCxu3YAQKrtQEKJKaUe7hzQ4WsK1j+StAk34gxpFIFNZiEavBnMtdE0pjyM/o9QZMxzUQxfNEMY6g3d9mlD0QinJRLFWAJQ1ACFQQt774Epw48RlUWG3ckZaOtZIfqwWPuVbYvuNVLGJD6EGbaNjJqyycpTD+19FjsH37qxxtK/KQ5D8uG8l6/Phx+MvO18GJ3pc7ScrueGj5YHBD8vulD8KsWd/H2xmFWyEqoLjze/2N3TCpsUmWsVBN6SdyRIXNBuvWb4CRkRHQUDrimFzT4yXq/v24ilmw/uDlrJT3iYasAQjRgrXg0OFO+M+JbuUjLVbZBO7lbXYb1DqNZdvK0kbG7w/Axs3bwIChrbSc0skzgQeqc775LVnlFQ1AAGQEyqcaLGZKjZYdNsag59OKjJVo5c9T/ldVV3PvG+Q8Sng0Hxjxcd9iSx+ftmwE8AGVmOZg1cLl4Iv5Jtq5jxxeMTJIFkE5Bv9Pc6ojoBxKU4rQZ+zFTiFFbk4pZgvRJjQiNACdFk5TI+VSuLXm7vekrsfwPiCNMCVv/RRkHze+QEfBQyaTOoIFbLaWzu2Yb+Vq5PNoJJIeHvGtDAZwLZNt2VqzxXYX7sPUvkiSpZabpMsWJhrGWynmSG6MvgUGiMfCT+LO6wabo7KpXOqT1VlckvAFxw7vYO/dfOZSfVdzqwtXnhu5NV8KqOjxLHgG+twBn+dJPqqgCOK+352Mx1fQffpYuPBBS+sTHc9gXyjkDy5TSyHOJpbFYtFQuWSgJRQjj5bGFQzDuPlyCAxAE4ODmg0sEz1czFLCJ8jvE+NwKAj+Yd+q0VHfCf6cXN/n6z+RiMef5naeEyyKZMR0OoVO6D/s8fS/kM+3wAAAp9hUgr0fwzYzMQ/gthgvH9yDfZ8PRoJP5TNWemYiwacxCj6b6E+iKI28HncmEPDfjzwLXnSIGADA7e5/M87GdsodIpQUIO+PDHshOOJfhle9QSX4/HlECSbZxPIE91ZI5CYjH0HkmRyIemD9ce/0+9xvioAIL0X5AGwy/QAbi4ZLiwINJPG19pBnYK/X27+dT7eYvtvdsz3GMntLvaYn2T0D/RG/z/+AFF/RCCDgEfx/HXxJsYbWbqmNiyRRfCWGyidG/f6lCJOSglMxnkrE2KXoxUSxjqDQD4+OYhQOPROJjEj+75GkAUi4cDb5JB6HexSPXzxNiDETjdKb3a1+v3c/b6qkrtc7sD8ei23lfhihsiCSsbJUfwZ6e8KhEcGyly+EvAEGBkZSifhDxJzqgZoPbaBCQf9wko08nM+s1OdMmn0YX7MP08qkRgaCoR1nJBR6KBwOy268FKtLe3u7MTDK7MAzeQTP4SrCOVuDvxL5x2D/qcdKVVgMr8nVci/KcCGmo19sXjCmAX06lbJXOazXd3V1Ff8DBwExfKhuba3MH5N5NrhcLovMfElTLS0tZkQU/ZGDGMEiZRYjcWbsjAW+Chb4L4a2YLN94TiHAAAAAElFTkSuQmCC'
8types/index.d.ts 8 lines1export type BasaltLink = { url: string; title: string; trail: string[]; by: string; at: number }
2
3declare module 'claude-code' {
4 interface PluginState {
5 basalt: { links: BasaltLink[] }
6 }
7}
8