diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml deleted file mode 100644 index 9e1bdee..0000000 --- a/.github/workflows/pages.yml +++ /dev/null @@ -1,53 +0,0 @@ -name: Site - -on: - workflow_dispatch: - push: - branches: - - main - paths: - - plugins/*/CHANGELOG.md - - plugins/*/README.md - - site/** - - docs/** - - plugins/*/.claude-plugin/plugin.json - - .claude-plugin/marketplace.json - - scripts/build-site.py - -permissions: - contents: read - -concurrency: - group: pages - cancel-in-progress: false - -jobs: - build: - if: github.ref == 'refs/heads/main' - runs-on: ubuntu-26.04 - timeout-minutes: 5 - - steps: - - uses: actions/checkout@v7 - - - name: Build site - run: scripts/build-site.py _site - - - uses: actions/upload-pages-artifact@v5 - with: - path: _site - - deploy: - needs: build - runs-on: ubuntu-26.04 - timeout-minutes: 5 - permissions: - pages: write - id-token: write - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - - steps: - - id: deployment - uses: actions/deploy-pages@v5 diff --git a/.gitignore b/.gitignore index 090b086..a953c9c 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,3 @@ *.tmp .env .env.* -_site/ diff --git a/README.md b/README.md index f720c67..c927015 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,11 @@

- - - - LinearB Agent Plugins: engineering context from LinearB, right inside your AI coding agent - - + + + LinearB Agent Plugins: engineering context from LinearB, right inside your AI coding agent +

- Website agentic-advisor version Claude Code plugin marketplace CI @@ -16,11 +13,10 @@

- Website · Quick start · How it works · Privacy · - Changelog · + Changelog · Security

@@ -32,7 +28,7 @@ AI agents write code the same way whether the target is a calm, well-tested modu | Plugin | What it does | Docs | | --- | --- | --- | -| [**agentic-advisor**](plugins/agentic-advisor) | Before writing code, grades how fragile the target is (LinearB rework, incidents and unreviewed merges, plus local git history on the files being touched) and holds the agent to a matching **LOW / MEDIUM / HIGH** effort level: lean on calm repos, defensive on fragile ones. | [Website](https://linear-b.github.io/agent-plugins/agentic-advisor/) · [README](plugins/agentic-advisor) | +| [**agentic-advisor**](plugins/agentic-advisor) | Before writing code, grades how fragile the target is (LinearB rework, incidents and unreviewed merges, plus local git history on the files being touched) and holds the agent to a matching **LOW / MEDIUM / HIGH** effort level: lean on calm repos, defensive on fragile ones. | [README](plugins/agentic-advisor) | ## Quick start @@ -86,7 +82,7 @@ Admins can install and enable the plugin for every developer from **Claude.ai ad The token in managed settings is pushed to every developer's machine. Use a token created for this purpose, so you can rotate it on its own. -**Stay up to date:** third-party marketplaces don't auto-update by default. Enable it in `/plugin` → Marketplaces → `linearb-ai` → Enable auto-update, or run `claude plugin update agentic-advisor@linearb-ai` and restart. See the [changelog](https://linear-b.github.io/agent-plugins/changelog/) for what changed in each release. +**Stay up to date:** third-party marketplaces don't auto-update by default. Enable it in `/plugin` → Marketplaces → `linearb-ai` → Enable auto-update, or run `claude plugin update agentic-advisor@linearb-ai` and restart. See the [changelog](plugins/agentic-advisor/CHANGELOG.md) for what changed in each release. ## What you'll see @@ -99,9 +95,7 @@ Start a code task (`fix the retry bug in billing/client.ts`) and the agent print ## How it works

- - What agentic-advisor looks at: LinearB repo health, incidents and ticket; local git history on the files you touch; and your request. Effort is the highest of the three. Only the grade and its metadata are reported to your own LinearB org, never code or prompts. - + What agentic-advisor looks at: LinearB repo health, incidents and ticket; local git history on the files you touch; and your request. Effort is the highest of the three. Only the grade and its metadata are reported to your own LinearB org, never code or prompts.

- **Repo health** comes from LinearB's public API: rework rate (bands match LinearB's own benchmark), unreviewed merges and recent incidents. It's cached per repo for 24 hours. @@ -109,7 +103,7 @@ Start a code task (`fix the retry bug in billing/client.ts`) and the agent print - **Change area** runs when the repo already grades MEDIUM/HIGH or the change touches sensitive code (auth, payments, migrations, concurrency, public API, crypto). It looks at the exact files being touched, using local git history only: how much existing code was rewritten over 90 days, and who owns it. - **It never blocks.** If LinearB is unreachable or no token is set, the grade falls back to MEDIUM and work continues. -See the [plugin page](https://linear-b.github.io/agent-plugins/agentic-advisor/) for triggers, the full grading rules and configuration. +See the [plugin README](plugins/agentic-advisor) for triggers, the full grading rules and configuration. ## Requirements diff --git a/plugins/agentic-advisor/README.md b/plugins/agentic-advisor/README.md index e3d54f6..fb78e25 100644 --- a/plugins/agentic-advisor/README.md +++ b/plugins/agentic-advisor/README.md @@ -4,7 +4,7 @@ Sizes the agent's coding **effort** to how fragile the target is — before it w At the start of a code task, a bundled hook auto-invokes the `agentic-advisor` skill. It reads the repo's current health (LinearB rework %, unreviewed merges, recent incidents) plus local git history on the files being touched, then grades **LOW / MEDIUM / HIGH effort** and holds the agent to it: lean and token-efficient on calm repos, defensive (smallest change, guards, tests, clarifying questions) on fragile ones. It never blocks; if LinearB data is unavailable it defaults to MEDIUM. -**Docs, diagrams and changelog:** [linear-b.github.io/agent-plugins/agentic-advisor](https://linear-b.github.io/agent-plugins/agentic-advisor/) +**Changelog:** [CHANGELOG.md](CHANGELOG.md) ## Install diff --git a/scripts/build-site.py b/scripts/build-site.py deleted file mode 100755 index cd46952..0000000 --- a/scripts/build-site.py +++ /dev/null @@ -1,852 +0,0 @@ -#!/usr/bin/env python3 -"""Build the GitHub Pages site: home, one page per plugin, changelog, Atom feed. - -Usage: scripts/build-site.py [out_dir] [repo_root] (defaults: _site, this repo) -Every plugin listed in .claude-plugin/marketplace.json gets a page from its README.md; -its CHANGELOG.md feeds the changelog. Standard library only. -""" -import html -import json -import re -import sys -from urllib.parse import urljoin -from pathlib import Path - -SITE_URL = "https://linear-b.github.io/agent-plugins/" -REPO_URL = "https://github.com/linear-b/agent-plugins" -MARKETPLACE = "linearb-ai" -# Diagrams: plugin READMEs hold ```mermaid blocks (GitHub renders them too); the site draws them hand-drawn. -MERMAID_JS = ("https://cdn.jsdelivr.net/npm/mermaid@12.1.0/dist/mermaid.min.js", - "sha384-EbBpjO7rlR6eqZEcG7GaPpyk9H9WrMyPWX4d3KvPYltgt8Z8l0z6R56B1qP40pR4") - -VERSION_RE = re.compile(r"^## \[([^\]]+)\](?:\s*-\s*(\d{4}-\d{2}-\d{2}))?\s*$") -SECTION_RE = re.compile(r"^### (.+?)\s*$") -ITEM_RE = re.compile(r"^[-*] (.*)$") - - -# ---------- parsing ---------- - -def parse_changelog(text): - """Keep-a-Changelog subset -> [{version, date, sections: [{name, items}]}].""" - releases, rel, sec = [], None, None - for line in text.splitlines(): - m = VERSION_RE.match(line) - if m: - rel = {"version": m.group(1), "date": m.group(2) or "", "sections": []} - releases.append(rel) - sec = None - continue - if rel is None or re.match(r"^\[[^\]]+\]: ", line): - continue - m = SECTION_RE.match(line) - if m: - sec = {"name": m.group(1), "items": []} - rel["sections"].append(sec) - continue - m = ITEM_RE.match(line) - if m: - if sec is None: # bullet without a ### heading - sec = {"name": "Changed", "items": []} - rel["sections"].append(sec) - sec["items"].append(m.group(1).strip()) - elif line.startswith(" ") and line.strip() and sec and sec["items"]: - sec["items"][-1] += " " + line.strip() # wrapped bullet - return releases - - -def slug(s): - return re.sub(r"[^a-z0-9]+", "-", s.lower()).strip("-") - - -def gh_slug(s): - """GitHub's heading anchor, so README #links keep working on the site.""" - s = re.sub(r"<[^>]+>", "", s).lower() - return re.sub(r"[^\w\- ]", "", s).replace(" ", "-") - - -def inline(md, link_base=None): - """Escape, then render `code`, [text](url) and **bold**. Relative links resolve against link_base.""" - codes = [] - - def keep_code(m): - codes.append("" + html.escape(m.group(1)) + "") - return "\x00%d\x00" % (len(codes) - 1) - - def keep_img(m): - url = html.unescape(m.group(2)) - if link_base and not re.match(r"^[a-z][a-z0-9+.-]*:", url, re.I): - url = urljoin(link_base, url) + "?raw=1" - elif not re.match(r"^https://", url): - return html.escape(m.group(1)) - codes.append('%s' % (html.escape(url, quote=True), html.escape(m.group(1), quote=True))) - return "\x00%d\x00" % (len(codes) - 1) - - s = re.sub(r"`([^`]+)`", keep_code, md) - s = re.sub(r"!\[([^\]]*)\]\(([^)\s]+)\)", keep_img, s) - s = html.escape(s) - - def link(m): - text, url = m.group(1), html.unescape(m.group(2)) - if url.startswith("#"): - pass - elif link_base and not re.match(r"^[a-z][a-z0-9+.-]*:", url, re.I): - url = (REPO_URL + "/blob/main" + url) if url.startswith("/") else urljoin(link_base, url) - elif not re.match(r"^https?://", url): - return m.group(0) - cls = ' class="pr"' if re.fullmatch(r"#\d+", text) else "" - return '%s' % (cls, html.escape(url, quote=True), text) - - s = re.sub(r"\[([^\]]+)\]\(([^)\s]+)\)", link, s) - s = re.sub(r"\(((?:]*>[^<]*(?:,\s*)?)+)\)", r"\1", s) # "(#10)" -> pill - s = re.sub(r"\*\*(.+?)\*\*", r"\1", s) - return re.sub(r"\x00(\d+)\x00", lambda m: codes[int(m.group(1))], s) - - -def figure(src, alt, src_dir, root): - """A README's own SVG, inlined so it takes the site's theme; a sibling .tall.svg is the phone layout.""" - if not src_dir or re.match(r"^[a-z][a-z0-9+.-]*:", src, re.I) or not src.endswith(".svg"): - return None - path = (src_dir / src).resolve() - if not path.is_file() or root.resolve() not in path.parents: - return None - out = [] - for f, cls in ((path, "wide"), (path.with_name(path.stem + ".tall.svg"), "tall")): - if not f.is_file(): - continue - svg = re.sub(r"<\?xml[^>]*>\s*", "", f.read_text()) - if re.search(r"]*\sclass="', svg): - svg = svg.replace("%s' % (" has-tall" if len(out) > 1 else "", "".join(out)) - - -def markdown(text, link_base=None, src_dir=None, root=None): - """README subset -> (html, [(id, h2 text)]). Raw HTML lines are dropped, never passed through.""" - lines, out, toc, i = text.splitlines(), [], [], 0 - il = lambda s: inline(s, link_base) # noqa: E731 - seen = {} - - def is_block_start(l): - return (not l.strip() or l.startswith(("#", ">", "|", "```", "<")) - or re.match(r"^\s*([-*]|\d+\.) ", l) is not None) - - while i < len(lines): - line = lines[i] - if not line.strip() or line.lstrip().startswith("<"): - i += 1 - continue - if line.startswith("```"): - lang, body = line[3:].strip(), [] - i += 1 - while i < len(lines) and not lines[i].startswith("```"): - body.append(lines[i]) - i += 1 - i += 1 - src = html.escape("\n".join(body)) - if lang == "mermaid": - out.append('
%s
' % src) - else: - out.append(code_block("\n".join(body))) - continue - m = re.match(r"^(#{1,6}) (.+?)\s*#*$", line) - if m: - level, title = len(m.group(1)), m.group(2) - hid = gh_slug(title) - n = seen.get(hid, 0) - seen[hid] = n + 1 - if n: - hid = "%s-%d" % (hid, n) - if level == 2: - toc.append((hid, re.sub(r"<[^>]+>", "", il(title)))) - out.append('%s' % (level, hid, il(title), level)) - i += 1 - continue - if line.startswith(">"): - quote = [] - while i < len(lines) and lines[i].startswith(">"): - quote.append(lines[i][1:].strip()) - i += 1 - out.append("

%s

" % il(" ".join(quote))) - continue - if line.startswith("|") and i + 1 < len(lines) and re.match(r"^\|[\s:|-]+\|?\s*$", lines[i + 1]): - cells = lambda l: [c.strip() for c in l.strip().strip("|").split("|")] # noqa: E731 - head = cells(line) - i += 2 - rows = [] - while i < len(lines) and lines[i].startswith("|"): - rows.append(cells(lines[i])) - i += 1 - out.append('
%s%s
' % ( - "".join("%s" % il(c) for c in head), - "".join("%s" % "".join("%s" % il(c) for c in r) for r in rows))) - continue - m = re.match(r"^([-*]|\d+\.) ", line) - if m: - ordered = m.group(1)[0].isdigit() - items = [] - while i < len(lines): - m2 = re.match(r"^([-*]|\d+\.) (.*)$", lines[i]) - if m2 and m2.group(1)[0].isdigit() == ordered: - items.append(m2.group(2)) - elif lines[i].startswith(" ") and lines[i].strip() and items: - items[-1] += " " + lines[i].strip() - elif not lines[i].strip() and i + 1 < len(lines) and re.match( - r"^\d+\. " if ordered else r"^[-*] ", lines[i + 1]): - pass # blank line between items of the same list - else: - break - i += 1 - tag = "ol" if ordered else "ul" - out.append("<%s>%s" % (tag, "".join("
  • %s
  • " % il(x) for x in items), tag)) - continue - m = re.match(r"^!\[([^\]]*)\]\(([^)\s]+)\)\s*$", line) - fig = m and figure(m.group(2), m.group(1), src_dir, root) - if fig: - out.append(fig) - i += 1 - continue - para = [] - while i < len(lines) and not (para and is_block_start(lines[i])): - para.append(lines[i].strip()) - i += 1 - out.append("

    %s

    " % il(" ".join(para))) - return "\n".join(out), toc - - -def split_readme(text): - """README -> (lede markdown, rest markdown): drop the H1, take the first paragraph as the lede.""" - lines = text.splitlines() - if lines and lines[0].startswith("# "): - lines = lines[1:] - while lines and not lines[0].strip(): - lines = lines[1:] - lede = [] - while lines and lines[0].strip() and not lines[0].startswith(("#", "```", ">", "|", "-")): - lede.append(lines.pop(0).strip()) - return " ".join(lede), "\n".join(lines) - - -def load_plugins(root): - """Only plugins the marketplace publishes, so every install command on the site works.""" - market = json.loads((root / ".claude-plugin/marketplace.json").read_text()) - plugins = [] - for entry in market.get("plugins", []): - d = (root / entry.get("source", "")).resolve() - manifest = d / ".claude-plugin/plugin.json" - if not manifest.is_file(): - continue - meta = json.loads(manifest.read_text()) - cl, readme = d / "CHANGELOG.md", d / "README.md" - plugins.append({ - "name": entry["name"], - "dir": d.name, - "path": d, - "version": meta.get("version", ""), - "description": entry.get("description") or meta.get("description", ""), - "readme": readme.read_text() if readme.is_file() else "", - "releases": parse_changelog(cl.read_text()) if cl.is_file() else [], - }) - return market, plugins - - -# ---------- rendering ---------- - -def managed_settings(names): - """Claude.ai admin console > Claude Code > Managed settings: installs and enables for the whole org.""" - return json.dumps({ - "env": {"LINEARB_API_TOKEN": " Settings > API Tokens>"}, - "extraKnownMarketplaces": {MARKETPLACE: { - "source": {"source": "github", "repo": REPO_URL.split("github.com/")[1]}, "autoUpdate": True}}, - "enabledPlugins": {"%s@%s" % (n, MARKETPLACE): True for n in names}, - }, indent=2) - - -def code_block(src): - return ('
    %s
    ' - % (html.escape(src), html.escape(src, quote=True))) - - -def rollout(names): - return """
    -

    Roll out to your whole org

    -

    Admins can install and enable this for every developer from Claude.ai admin console → Claude Code → Managed settings. Paste this, then replace the token placeholder.

    -

    The token in managed settings is pushed to every developer's machine. Use a token created for this purpose, so you can rotate it on its own.

    - %s
    """ % code_block(managed_settings(names)) - - -ICON_SPARK = ('') -ICON_TERM = ('') -ICON_COPY = ('') -ICON_KEY = ('') - - -def install_panel(names): - """Header 'Install' popover: a prompt for Claude Code, or one terminal command.""" - ids = ["%s@%s" % (n, MARKETPLACE) for n in names] - repo = REPO_URL.split("github.com/")[1] - cmd = " && ".join(["claude plugin marketplace add %s" % repo] + ["claude plugin install %s" % i for i in ids]) - what = names[0] if len(names) == 1 else "the LinearB plugins" - prompt = ("Install %s for me: run `claude plugin marketplace add %s`, then %s. " - "Then check whether LINEARB_API_TOKEN is set in my environment. If it isn't, tell me how to create one " - "(LinearB > Settings > API Tokens) and export it before restarting Claude Code." - % (what, repo, ", ".join("`claude plugin install %s`" % i for i in ids))) - return """""".format(key=ICON_KEY, spark=ICON_SPARK, term=ICON_TERM, copy=ICON_COPY, - prompt=html.escape(prompt, quote=True), cmd=html.escape(cmd, quote=True), cmd_text=html.escape(cmd)) - - -def rel_id(p, r): - return "%s-v%s" % (slug(p["name"]), slug(r["version"])) - - -def install_box(cmd): - return ('
    %s
    ' - % (html.escape(cmd), html.escape(cmd, quote=True))) - - -def render_release(p, r, latest, base): - rid = rel_id(p, r) - out = ['
    ' % rid, '
    ', - '' - % (rid, html.escape(r["version"])), - '

    #%s

    ' - % (rid, html.escape(r["version"]), html.escape(r["version"]))] - if latest: - out.append('Latest') - if r["date"]: - out.append('' % (r["date"], r["date"])) - tag = "%s--v%s" % (p["name"], r["version"]) - out.append('Release notes' - % (REPO_URL, html.escape(tag, quote=True))) - out.append('
    ' % rid) - for s in r["sections"]: - k = slug(s["name"]) - out.append('

    %s

      ' - % (k, k, html.escape(s["name"]))) - out.extend("
    • %s
    • " % inline(i) for i in s["items"]) - out.append("
    ") - out.append("
    ") - return "\n".join(out) - - -def logo(root): - svg = (root / "site/assets/linearb-logo.svg").read_text() - return re.sub(r'\s(width|height)="[^"]*"', "", svg, count=2).replace("Updated ' % (latest["date"], latest["date"]) - if latest and latest["date"] else "") - cards.append( - '' - % (p["dir"], html.escape(p["name"]), html.escape(p["version"]), html.escape(p["description"]), meta, - install_box("/plugin install %s@%s" % (p["name"], MARKETPLACE)), p["dir"], - 'Changelog' % slug(p["name"]) if p["releases"] else "")) - news = [] - for p, r in sorted(((p, r) for p in plugins for r in p["releases"][:3]), - key=lambda x: (x[1]["date"], x[1]["version"]), reverse=True)[:4]: - first_item = next((s["items"][0] for s in r["sections"] if s["items"]), "") - summary = re.sub(r"\s*\(\[#\d+\]\([^)]*\)\)", "", first_item) - news.append('
  • %s %s

    %s

  • ' - % (rel_id(p, r), html.escape(p["name"]), html.escape(r["version"]), - r["date"], r["date"], inline(summary))) - body = """ -
    -

    Claude Code plugins

    -

    Engineering context from LinearB, right inside your AI coding agent.

    -

    Plugins that give your agent the context a senior engineer would have: how healthy the repo is, how fragile the files are, and how much care the change deserves.

    - {box} - -
    -

    Quick start

      -
    1. Add the marketplace

      In Claude Code, run /plugin marketplace add linear-b/agent-plugins.

    2. -
    3. Install a plugin

      For example /plugin install {first}@{mk}.

    4. -
    5. Connect LinearB

      Create a token in LinearB → Settings → API Tokens, export LINEARB_API_TOKEN=…, then restart Claude Code.

    6. -
    {rollout}
    -

    Plugins

    {cards}
    -
    -

    What's new

      {news}
    Full changelog →
    -

    Built to stay out of the way

    -
    -
    Your data stays in your org
    Usage is reported only to the LinearB org your token belongs to. Source code and full prompts are never sent.
    -
    Never blocks
    If LinearB can't be reached, plugins fall back to safe defaults and your agent keeps working.
    -
    Open source
    Apache-2.0. Report vulnerabilities to security@linearb.io, per SECURITY.md.
    -
    -
    -
    -""".format(box=install_box("/plugin marketplace add linear-b/agent-plugins"), - first=html.escape(first), mk=MARKETPLACE, rollout=rollout([p["name"] for p in plugins]), cards="".join(cards), news="".join(news), repo=REPO_URL) - return page(root, "LinearB Agent Plugins", desc, body, "", "home", [p["name"] for p in plugins]) - - -def render_plugin(root, p): - base = "../" - lede, rest = split_readme(p["readme"]) - # A README line pointing at this very page is for GitHub readers; drop it here. - rest = "\n".join(l for l in rest.splitlines() if "%s%s/)" % (SITE_URL, p["dir"]) not in l) - link_base = "%s/blob/main/plugins/%s/" % (REPO_URL, p["dir"]) - body_html, toc = markdown(rest, link_base, p["path"], root) - toc_html = "".join('
  • %s
  • ' % (h, t) for h, t in toc) - recent, recent_toc, cl_btn = "", "", "" - if p["releases"]: # a plugin without a CHANGELOG gets no changelog links - recent = ('

    Recent releases

    %s
    ' - 'All releases →
    ' - % ("".join(render_release(p, r, i == 0, base) for i, r in enumerate(p["releases"][:3])), slug(p["name"]))) - recent_toc = '
  • Recent releases
  • ' - cl_btn = 'Changelog' % slug(p["name"]) - body = """ -
    - -
    -

    Plugins / {name}

    -

    {name}

    v{ver}
    -

    {lede}

    - {install} -
    Rolling out to a team?{rollout}
    -
    {cl_btn}Source
    -
    {body}
    - {recent} -
    -""".format(toc=toc_html, name=html.escape(p["name"]), ver=html.escape(p["version"]), lede=inline(lede, link_base), - install=install_box("/plugin install %s@%s" % (p["name"], MARKETPLACE)), rollout=rollout([p["name"]]), - recent_toc=recent_toc, cl_btn=cl_btn, repo=REPO_URL, dir=p["dir"], body=body_html, recent=recent) - head, scripts = "", "" - if 'class="mermaid"' in body_html or 'class="figure' in body_html: - head = '' - if 'class="mermaid"' in body_html: - scripts = '' % ( - MERMAID_JS[0], MERMAID_JS[1], DIAGRAM_JS) - return page(root, "%s · LinearB Agent Plugins" % p["name"], p["description"], body, base, "plugin", [p["name"]], head, scripts) - - -def render_changelog(root, plugins): - base = "../" - kinds = [] - for p in plugins: - for r in p["releases"]: - for s in r["sections"]: - if s["name"] not in kinds: - kinds.append(s["name"]) - chips = "".join('' - % (slug(k), slug(k), html.escape(k)) for k in kinds) - nav, main = [], [] - for p in plugins: - if not p["releases"]: - continue - nav.append('

    %s

      ' % html.escape(p["name"])) - nav.extend('
    1. %s%s
    2. ' - % (rel_id(p, r), html.escape(r["version"]), html.escape(r["date"])) for r in p["releases"]) - nav.append("
    ") - main.append('

    %s

    ' - 'v%s
    %s
    ' - % (slug(p["name"]), base, p["dir"], html.escape(p["name"]), html.escape(p["version"]), - "".join(render_release(p, r, i == 0, base) for i, r in enumerate(p["releases"])))) - body = """ -
    - -
    -

    Changelog

    What changed in each release, newest first. Subscribe with the Atom feed.

    -
    {chips}
    -{main} -

    No changes match. Clear filters

    -
    -""".format(nav="\n".join(nav), chips=chips, main="\n".join(main)) - return page(root, "Changelog · LinearB Agent Plugins", "What changed in each release of LinearB's Claude Code plugins.", - body, base, "changelog", [p["name"] for p in plugins], "", "" % CHANGELOG_JS) - - -def render_feed(plugins): - entries, newest = [], "1970-01-01" - for p in plugins: - for r in p["releases"]: - date = r["date"] or "1970-01-01" - newest = max(newest, date) - url = "%schangelog/#%s" % (SITE_URL, rel_id(p, r)) - content = "".join("

    %s

      %s
    " % (html.escape(s["name"]), - "".join("
  • %s
  • " % inline(i) for i in s["items"])) - for s in r["sections"]) - entries.append((date, "%s %s%s" - '%sT00:00:00Z' - '%s' - % (html.escape(p["name"]), html.escape(r["version"]), url, url, date, - html.escape(content)))) - entries.sort(key=lambda e: e[0], reverse=True) - return ('\n' - 'LinearB Agent Plugins changelog' - '%sLinearB' - '%sT00:00:00Z%s\n' - % (SITE_URL, SITE_URL, SITE_URL, newest, "".join(e[1] for e in entries))) - - -# ---------- assets ---------- - -# LinearB "Buzz" design system (linweb): Outfit, slate primary, bright-blue brand, Buzz status colors. -CSS = r""" -:root{--dg-canvas:transparent;--dg-purple:#7c5cff;--dg-purple-soft:#f1ecff;--claude:#c4623f;--claude-btn:#d97757;--claude-soft:#fbeee8;--bg:#ffffff;--surface:#f5f6fa;--card:#ffffff;--fg:#0a0a0a;--primary:#383f48;--muted:#61656c;--line:#e4e4e7; ---brand:#5467ff;--brand-strong:#1831f4;--brand-soft:#eef0ff;--on-brand:#ffffff;--code:#f1f2f6; ---success:#15803d;--success-soft:#e7f6ec;--attention:#b45309;--attention-soft:#fef3e2;--destructive:#bf1245;--destructive-soft:#fce8ee; ---font:"Outfit",ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;--mono:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; ---radius:12px;color-scheme:light} -@media (prefers-color-scheme:dark){:root:not([data-theme="light"]){--dg-purple:#a18cff;--dg-purple-soft:#221a3d;--claude:#e8957a;--claude-soft:#2a1d18;--bg:#0a0a0a;--surface:#111113;--card:#161618;--fg:#fafafa;--primary:#e4e4e7;--muted:#a1a1aa;--line:#262626; ---brand:#7c88ff;--brand-strong:#9aa3ff;--brand-soft:#1c1f45;--on-brand:#0a0a0a;--code:#1d1d21; ---success:#4ade80;--success-soft:#11281a;--attention:#fbbf24;--attention-soft:#2e2209;--destructive:#fb7194;--destructive-soft:#331520;color-scheme:dark}} -:root[data-theme="dark"]{--dg-purple:#a18cff;--dg-purple-soft:#221a3d;--claude:#e8957a;--claude-soft:#2a1d18;--bg:#0a0a0a;--surface:#111113;--card:#161618;--fg:#fafafa;--primary:#e4e4e7;--muted:#a1a1aa;--line:#262626; ---brand:#7c88ff;--brand-strong:#9aa3ff;--brand-soft:#1c1f45;--on-brand:#0a0a0a;--code:#1d1d21; ---success:#4ade80;--success-soft:#11281a;--attention:#fbbf24;--attention-soft:#2e2209;--destructive:#fb7194;--destructive-soft:#331520;color-scheme:dark} -*{box-sizing:border-box} -html{scroll-behavior:smooth;scroll-padding-top:84px} -@media (prefers-reduced-motion:reduce){html{scroll-behavior:auto}*{transition:none!important}} -body{margin:0;background:var(--bg);color:var(--fg);font:16px/1.65 var(--font);-webkit-font-smoothing:antialiased} -a{color:var(--brand-strong);text-decoration:none}a:hover{text-decoration:underline} -:focus-visible{outline:2px solid var(--brand);outline-offset:2px;border-radius:4px} -h1,h2,h3{color:var(--fg);text-wrap:balance;letter-spacing:-.015em;line-height:1.2} -code{font:.85em var(--mono);background:var(--code);padding:1px 6px;border-radius:6px;word-break:break-word} -.top{position:sticky;top:0;z-index:5;background:color-mix(in srgb,var(--bg) 88%,transparent);backdrop-filter:blur(10px);border-bottom:1px solid var(--line)} -.top-in{max-width:1160px;margin:0 auto;padding-inline:20px;height:64px;display:flex;align-items:center;gap:20px} -.brand{display:flex;align-items:center;gap:12px;color:var(--fg);font-weight:500} -.brand svg{height:22px;width:auto;display:block} -.brand span{padding-left:12px;border-left:1px solid var(--line);color:var(--muted)} -.brand:hover{text-decoration:none} -.top nav{display:flex;gap:4px;margin-left:auto;align-items:center} -.top nav a{color:var(--muted);padding:6px 12px;border-radius:8px;font-size:15px} -.top nav a:hover,.top nav a[aria-current]{color:var(--fg);background:var(--surface);text-decoration:none} -.icon-btn{border:1px solid var(--line);background:var(--card);color:var(--fg);border-radius:8px;width:34px;height:34px;cursor:pointer;font:inherit} -.top-in{position:relative} -.install-btn{display:inline-flex;align-items:center;gap:8px;margin-left:6px;border:0;border-radius:10px;padding:8px 14px;background:var(--fg);color:var(--bg);font:600 15px var(--font);cursor:pointer} -.chev{width:6px;height:6px;border-right:2px solid currentColor;border-bottom:2px solid currentColor;transform:rotate(45deg);margin-top:-3px;transition:transform .15s} -.install-btn[aria-expanded="true"] .chev{transform:rotate(-135deg);margin-top:3px} -.pop{position:absolute;right:20px;top:58px;width:min(560px,calc(100vw - 32px));background:var(--card);border:1px solid var(--line);border-radius:18px;padding:16px;box-shadow:0 24px 60px -20px rgb(0 0 0/.35);z-index:10} -.callout{display:flex;gap:12px;background:var(--attention-soft);color:var(--fg);border-radius:12px;padding:14px 16px;font-size:14.5px;line-height:1.5} -.callout svg{width:22px;height:22px;flex:none;color:var(--attention);margin-top:1px} -.callout strong{display:block;color:var(--attention)} -.ways{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:12px} -.way{background:var(--surface);border-radius:14px;padding:16px;min-width:0;display:flex;flex-direction:column} -.way.claude{background:linear-gradient(160deg,var(--claude-soft),var(--surface) 70%)} -.way-label{display:flex;align-items:center;gap:8px;margin:0;font-size:13px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--muted)} -.way.claude .way-label{color:var(--claude)} -.way-label svg{width:20px;height:20px} -.way-title{margin:6px 0 14px;font-size:17px;font-weight:500;color:var(--muted)} -.way-btn{display:flex;align-items:center;justify-content:center;gap:10px;border:0;border-radius:11px;padding:12px;font:600 15.5px var(--font);cursor:pointer;background:var(--claude-btn);color:#1a0f0a} -.way-btn svg{width:20px;height:20px} -.way-btn.dark{background:var(--fg);color:var(--bg)} -.way-btn:hover{filter:brightness(1.08)} -.way-hint{margin:8px 0 0;font-size:13.5px;color:var(--muted)} -.way-hint.mono{font-family:var(--mono);font-size:12.5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} -.pop-foot{margin:12px 2px 2px;font-size:14px;color:var(--muted)} -.wrap{max-width:1160px;margin:0 auto;padding:40px 20px 72px} -.btn{display:inline-flex;align-items:center;border-radius:9px;padding:9px 16px;font:500 15px var(--font);background:var(--primary);color:var(--bg);border:1px solid transparent;cursor:pointer} -.btn:hover{text-decoration:none;filter:brightness(1.15)} -.btn.ghost{background:transparent;color:var(--fg);border-color:var(--line)} -.btn.ghost:hover{border-color:var(--brand);filter:none} -.btn.small{padding:6px 12px;font-size:13px} -.pill{font-size:12px;font-weight:600;border-radius:999px;padding:2px 10px;white-space:nowrap} -.ver{background:var(--brand-soft);color:var(--brand-strong)} -.latest{background:var(--success-soft);color:var(--success)} -.muted{color:var(--muted);font-size:14px} -.eyebrow{text-transform:uppercase;letter-spacing:.12em;font-size:13px;font-weight:600;color:var(--brand-strong);margin:0 0 14px} -.lede{color:var(--muted);font-size:18px;max-width:62ch;margin:0 0 22px} -.install{display:flex;align-items:center;gap:8px;background:var(--code);border:1px solid var(--line);border-radius:10px;padding:6px 6px 6px 14px;max-width:560px} -.install code{background:none;padding:0;flex:1;min-width:0;overflow-x:auto;white-space:nowrap;font-size:14px} -.copy{border:0;background:var(--brand);color:var(--on-brand);border-radius:7px;padding:7px 14px;font:600 13px var(--font);cursor:pointer} -.copy.ghost{background:var(--card);color:var(--muted);border:1px solid var(--line)} -.hero-actions{display:flex;flex-wrap:wrap;gap:10px;margin-top:20px} -/* home */ -.hero{padding-block:36px 56px;max-width:820px} -.hero h1{font-size:clamp(34px,5vw,54px);font-weight:600;margin:0 0 18px} -section+section{margin-top:56px} -.wrap>section>h2,.split h2{font-size:26px;margin:0 0 20px} -.steps ol{list-style:none;counter-reset:s;padding:0;margin:0;display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:16px} -.steps li{counter-increment:s;background:var(--surface);border-radius:var(--radius);padding:22px;min-width:0} -.steps li:before{content:counter(s);display:grid;place-items:center;width:30px;height:30px;border-radius:50%;background:var(--brand);color:var(--on-brand);font-weight:600;margin-bottom:12px} -.steps h3{margin:0 0 6px;font-size:18px}.steps p{margin:0;color:var(--muted)} -.cards{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(100%,420px),1fr));gap:18px} -.card{background:var(--card);border:1px solid var(--line);border-radius:var(--radius);padding:24px;display:flex;flex-direction:column;gap:14px;min-width:0} -.card-top{display:flex;align-items:center;gap:10px} -.card h3{margin:0;font-size:22px}.card h3 a{color:var(--fg)} -.card p{margin:0;color:var(--muted)} -.card-actions{display:flex;flex-wrap:wrap;gap:10px;margin-top:auto} -.split{display:grid;grid-template-columns:minmax(0,1.2fr) minmax(0,1fr);gap:40px} -.news{list-style:none;padding:0;margin:0 0 14px} -.news li{padding:14px 0;border-bottom:1px solid var(--line);display:grid;grid-template-columns:auto 1fr;gap:2px 12px} -.news-ver{font-weight:600}.news time{color:var(--muted);font-size:14px;justify-self:end} -.news p{grid-column:1/-1;margin:0;color:var(--muted);font-size:15px} -.trust{align-self:start;background:var(--surface);border-radius:var(--radius);padding:24px 26px} -.trust dl{margin:0}.trust dt{font-weight:600;margin-top:16px}.trust dt:first-child{margin-top:0}.trust dd{margin:2px 0 0;color:var(--muted)} -/* docs + changelog layout */ -.doc{display:grid;grid-template-columns:210px minmax(0,1fr);gap:48px} -.side{position:sticky;top:88px;align-self:start;max-height:calc(100vh - 110px);overflow:auto} -.side-label{font-size:12px;text-transform:uppercase;letter-spacing:.08em;color:var(--muted);margin:0 0 10px;font-weight:600} -.toc,.vers{list-style:none;margin:0 0 22px;padding:0;border-left:2px solid var(--line)} -.toc a,.vers a{display:flex;justify-content:space-between;gap:8px;padding:5px 12px;margin-left:-2px;border-left:2px solid transparent;color:var(--muted);font-size:14px} -.toc a:hover,.vers a:hover{color:var(--fg);text-decoration:none} -.toc a.active,.vers a.active{color:var(--brand-strong);border-left-color:var(--brand);font-weight:600} -.vers a.dim{opacity:.35}.vers small{font-size:12px} -.crumb{font-size:14px;color:var(--muted);margin:0 0 10px} -.doc-hero{padding-bottom:28px;border-bottom:1px solid var(--line);margin-bottom:8px} -.hero-top{display:flex;align-items:center;gap:12px;flex-wrap:wrap} -.doc-hero h1,.intro h1{font-size:clamp(32px,4vw,44px);margin:0;font-weight:600} -.doc-hero .lede{margin-top:12px} -.intro .lede{margin-top:8px} -.prose>*{max-width:76ch}.prose>.diagram,.prose>.table,.prose>.figure{max-width:none} -.prose h2{font-size:26px;margin:44px 0 12px;padding-top:4px} -.prose h3{font-size:19px;margin:28px 0 8px} -.prose p,.prose li{color:var(--fg)} -.prose ul,.prose ol{padding-left:22px}.prose li{margin:6px 0} -.prose blockquote{margin:14px 0;padding:12px 16px;background:var(--surface);border-radius:10px;font-family:var(--mono);font-size:14px;color:var(--primary)} -.prose blockquote p{margin:0} -.codeblock{position:relative;margin:16px 0} -.codeblock pre{margin:0;background:var(--code);border:1px solid var(--line);border-radius:10px;padding:14px 16px;overflow-x:auto} -.codeblock pre code{background:none;padding:0;font-size:14px;white-space:pre;word-break:normal} -.codeblock .copy{position:absolute;top:8px;right:8px;opacity:0;transition:opacity .15s} -.codeblock:hover .copy,.codeblock .copy:focus-visible{opacity:1} -.table{overflow-x:auto;margin:16px 0;border:1px solid var(--line);border-radius:10px} -table{border-collapse:collapse;width:100%;font-size:14.5px} -th,td{text-align:left;padding:10px 14px;border-bottom:1px solid var(--line);vertical-align:top} -th{background:var(--surface);font-weight:600}tr:last-child td{border-bottom:0} -.diagram{margin:22px 0;background:var(--surface);border-radius:var(--radius);padding:20px;overflow-x:auto;text-align:center} -.diagram pre.mermaid{margin:0;font-family:var(--mono);font-size:13px;text-align:left;white-space:pre;color:var(--muted)} -.diagram pre.mermaid[data-processed]{text-align:center;white-space:normal} -.diagram svg{max-width:min(100%,720px);height:auto} -.rollout{margin-top:18px;display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.1fr);gap:24px;align-items:start;background:var(--card);border:1px solid var(--line);border-radius:var(--radius);padding:22px} -.rollout h3{margin:0 0 8px;font-size:18px}.rollout p{margin:0 0 10px;color:var(--muted)} -.rollout .codeblock{margin:0} -.note{font-size:14px;border-left:3px solid var(--attention);padding-left:10px} -details.team{margin-top:14px;max-width:760px} -details.team summary{cursor:pointer;color:var(--brand-strong);font-weight:500;width:max-content} -details.team .rollout{grid-template-columns:minmax(0,1fr)} -.figure{margin:24px 0}.figure svg{display:block;width:100%;height:auto} -.figure .tall{display:none;max-width:600px;margin:0 auto} -@media (max-width:700px){.figure.has-tall .wide{display:none}.figure.has-tall .tall{display:block}} -.prose img{max-width:100%;height:auto} -.recent{margin-top:56px}.recent h2{font-size:26px;margin:0 0 18px} -/* changelog */ -.toolbar{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin:24px 0} -.search{flex:1 1 240px;min-width:0;border:1px solid var(--line);background:var(--card);color:var(--fg);border-radius:10px;padding:10px 14px;font:inherit;font-size:15px} -.search:focus{outline:2px solid var(--brand);outline-offset:-1px;border-color:transparent} -.cl-plugin+.cl-plugin{margin-top:48px} -.cl-head{display:flex;align-items:center;gap:10px;margin-bottom:16px}.cl-head h2{margin:0;font-size:24px}.cl-head h2 a{color:var(--fg)} -.chip{display:inline-block;font-size:12px;font-weight:600;border-radius:999px;padding:3px 11px;border:1px solid transparent;background:var(--surface);color:var(--muted)} -.chip-added{background:var(--success-soft);color:var(--success)}.chip-changed{background:var(--brand-soft);color:var(--brand-strong)} -.chip-fixed{background:var(--attention-soft);color:var(--attention)}.chip-removed,.chip-security,.chip-deprecated{background:var(--destructive-soft);color:var(--destructive)} -button.filter{cursor:pointer;font-family:inherit} -button.filter[aria-pressed="true"]{border-color:currentColor;box-shadow:0 0 0 1px currentColor inset} -.timeline{position:relative;padding-left:24px} -.timeline:before{content:"";position:absolute;left:5px;top:8px;bottom:8px;width:2px;background:var(--line)} -.release{position:relative;background:var(--card);border:1px solid var(--line);border-radius:var(--radius);padding:16px 22px;margin-bottom:14px} -.release:before{content:"";position:absolute;left:-24px;top:23px;width:12px;height:12px;border-radius:50%;background:var(--bg);border:2px solid var(--brand)} -.release:target{border-color:var(--brand);box-shadow:0 0 0 3px var(--brand-soft)} -.rel-head{display:flex;flex-wrap:wrap;align-items:center;gap:10px;cursor:pointer} -.rel-head h3{margin:0;font-size:19px} -.rel-head time{color:var(--muted);font-size:14px} -.rel-head .gh{margin-left:auto;font-size:14px} -.anchor{opacity:0;margin-left:-14px;width:14px;display:inline-block;color:var(--muted)} -.rel-head h3:hover .anchor,.anchor:focus-visible{opacity:1} -.fold{border:0;background:none;color:var(--muted);width:24px;height:24px;margin-left:-6px;border-radius:6px;cursor:pointer;display:grid;place-items:center;padding:0} -.fold:before{content:"";width:7px;height:7px;border-right:2px solid currentColor;border-bottom:2px solid currentColor;transform:rotate(45deg);margin-top:-3px;transition:transform .15s} -.fold:hover{background:var(--surface);color:var(--fg)} -.release.collapsed .fold:before{transform:rotate(-45deg);margin-top:0;margin-left:-3px} -.release.collapsed .rel-body{display:none} -.kind h4{margin:14px 0 6px}.kind ul{margin:0;padding-left:20px}.kind li{margin:5px 0}.kind li::marker{color:var(--muted)} -a.pr{font-size:12px;font-weight:600;background:var(--brand-soft);border-radius:999px;padding:1px 8px} -mark{background:#ffe58a;color:#0a0a0a;border-radius:3px;padding:0 1px} -.empty{display:none;text-align:center;color:var(--muted);padding:40px 0} -.hidden{display:none!important} -footer{border-top:1px solid var(--line);color:var(--muted);font-size:14px} -.foot{max-width:1160px;margin:0 auto;padding:24px 20px;display:flex;flex-wrap:wrap;gap:8px 20px;align-items:center} -.foot .spacer{flex:1} -@media (max-width:860px){.doc{grid-template-columns:minmax(0,1fr);gap:0}.side{display:none}.split,.rollout{grid-template-columns:minmax(0,1fr)} -.brand span{display:none}.wrap{padding-top:24px}.release{padding:14px 16px}.rel-head .gh{margin-left:0}.search{flex-basis:100%}.top nav a{padding:6px 8px}} -@media (max-width:860px){.top nav .gh-link{display:none}} -@media (max-width:560px){.ways{grid-template-columns:1fr}.pop{right:16px;top:60px}} -@media (max-width:480px){.top-in{gap:8px}.top nav{gap:0}.top nav>a{display:none}} -""" - -SHELL = r""" - - - - -{{TITLE}} - - - - - -{{HEAD}} - - - - -
    -{{LOGO}}Agent Plugins - -{{INSTALL}} -
    -
    -{{BODY}} -
    - - -{{SCRIPTS}} - - -""" - -JS = r""" -(function(){ - var root=document.documentElement; - function save(k,v){try{localStorage.setItem(k,v)}catch(e){}} - document.getElementById("theme").onclick=function(){ - var cur=root.getAttribute("data-theme"),dark=cur?cur==="dark":matchMedia("(prefers-color-scheme: dark)").matches; - var next=dark?"light":"dark";root.setAttribute("data-theme",next);save("theme",next); - document.dispatchEvent(new Event("themechange"))}; - document.querySelectorAll(".copy,.way-btn").forEach(function(b){b.onclick=function(e){ - e.stopPropagation();var t=b.querySelector("span")||b,label=t.textContent; - var done=function(){t.textContent="Copied";setTimeout(function(){t.textContent=label},1500)}; - if(navigator.clipboard)navigator.clipboard.writeText(b.dataset.copy).then(done,function(){})}}); - // Install popover. - var ib=document.getElementById("install-btn"),pop=document.getElementById("install-pop"); - function openPop(o){pop.hidden=!o;ib.setAttribute("aria-expanded",o?"true":"false")} - ib.onclick=function(e){e.stopPropagation();openPop(pop.hidden)}; - document.addEventListener("click",function(e){if(!pop.hidden&&!pop.contains(e.target))openPop(false)}); - document.addEventListener("keydown",function(e){if(e.key==="Escape"&&!pop.hidden){openPop(false);ib.focus()}}); - // Relative dates, absolute on hover. - var now=Date.now();document.querySelectorAll("time[datetime]").forEach(function(el){ - var d=Math.floor((now-new Date(el.dateTime+"T00:00:00Z"))/864e5);if(isNaN(d)||d<0)return; - el.title=el.dateTime;el.textContent=d===0?"today":d===1?"yesterday":d<30?d+" days ago":el.dateTime}); - // Highlight the section in view in the sidebar. - var links={};document.querySelectorAll(".side a[href^='#']").forEach(function(a){links[a.getAttribute("href").slice(1)]=a}); - var targets=Object.keys(links).map(function(id){return document.getElementById(id)}).filter(Boolean); - if(targets.length&&"IntersectionObserver"in window){var io=new IntersectionObserver(function(es){es.forEach(function(e){ - if(e.isIntersecting){Object.keys(links).forEach(function(k){links[k].classList.remove("active")});links[e.target.id].classList.add("active")}})},{rootMargin:"-90px 0px -65% 0px"}); - targets.forEach(function(t){io.observe(t)})} - // Collapsible releases. - var rels=[].slice.call(document.querySelectorAll(".release")),foldAll=document.getElementById("foldall"); - window.setFold=function(r,closed){r.classList.toggle("collapsed",closed);var b=r.querySelector(".fold"); - b.setAttribute("aria-expanded",closed?"false":"true");b.setAttribute("aria-label",(closed?"Expand ":"Collapse ")+r.querySelector("h3").lastChild.textContent);syncAll()}; - function syncAll(){if(foldAll)foldAll.textContent=rels.every(function(r){return r.classList.contains("collapsed")})?"Expand all":"Collapse all"} - rels.forEach(function(r){r.querySelector(".rel-head").addEventListener("click",function(e){ - if(e.target.closest("a"))return;setFold(r,!r.classList.contains("collapsed"))})}); - if(foldAll)foldAll.onclick=function(){var close=foldAll.textContent==="Collapse all";rels.forEach(function(r){setFold(r,close)})}; - function openHash(){var r=location.hash&&document.getElementById(location.hash.slice(1));if(r&&r.classList.contains("release"))setFold(r,false)} - window.addEventListener("hashchange",openHash);openHash(); -})(); -""" - -CHANGELOG_JS = r""" -(function(){ - var q=document.getElementById("q"),empty=document.getElementById("empty"),active={}; - var links={};document.querySelectorAll(".side a").forEach(function(a){links[a.getAttribute("href").slice(1)]=a}); - var items=[].slice.call(document.querySelectorAll(".kind li"));items.forEach(function(li){li._html=li.innerHTML}); - function esc(s){return s.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")} - // Wrap matches in , text nodes only, so links and code stay intact. - function highlight(node,re){[].slice.call(node.childNodes).forEach(function(n){ - if(n.nodeType===1){highlight(n,re);return} - if(n.nodeType!==3)return; - var parts=n.data.split(re);if(parts.length<2)return; - var frag=document.createDocumentFragment(); - parts.forEach(function(p,i){if(!p)return;if(i%2){var m=document.createElement("mark");m.textContent=p;frag.appendChild(m)}else frag.appendChild(document.createTextNode(p))}); - n.replaceWith(frag)})} - function apply(){ - var term=q.value.trim(),kinds=Object.keys(active).filter(function(k){return active[k]}),re=term?new RegExp("("+esc(term)+")","i"):null,shown=0; - items.forEach(function(li){ - var kindOk=!kinds.length||kinds.indexOf(li.closest(".kind").dataset.kind)>=0; - var textOk=!re||li.textContent.toLowerCase().indexOf(term.toLowerCase())>=0; - li.innerHTML=li._html; - if(re&&textOk)highlight(li,re); - li.classList.toggle("hidden",!(kindOk&&textOk))}); - document.querySelectorAll(".kind").forEach(function(s){s.classList.toggle("hidden",!s.querySelector("li:not(.hidden)"))}); - document.querySelectorAll(".release").forEach(function(r){var v=!!r.querySelector(".kind:not(.hidden)"); - r.classList.toggle("hidden",!v);if(links[r.id])links[r.id].classList.toggle("dim",!v); - if(v&&(term||kinds.length))setFold(r,false);if(v)shown++}); - document.querySelectorAll(".cl-plugin").forEach(function(s){s.classList.toggle("hidden",!s.querySelector(".release:not(.hidden)"))}); - empty.style.display=shown?"none":"block"} - q.addEventListener("input",apply); - document.querySelectorAll("button.filter").forEach(function(b){b.onclick=function(){ - active[b.dataset.kind]=!active[b.dataset.kind];b.setAttribute("aria-pressed",active[b.dataset.kind]?"true":"false");apply()}}); - document.getElementById("clear").onclick=function(e){e.preventDefault();q.value="";active={}; - document.querySelectorAll("button.filter").forEach(function(b){b.setAttribute("aria-pressed","false")});apply()}; - document.addEventListener("keydown",function(e){if(e.key==="/"&&document.activeElement!==q){e.preventDefault();q.focus()} - else if(e.key==="Escape"&&document.activeElement===q){q.value="";apply();q.blur()}}); -})(); -""" - -# Hand-drawn (Excalidraw-style) diagrams in the site's colors; redrawn when the theme flips. -DIAGRAM_JS = r""" -(function(){ - if(!window.mermaid)return; // CDN blocked: the diagram source stays readable as text - var blocks=[].slice.call(document.querySelectorAll("pre.mermaid")); - blocks.forEach(function(b){b.dataset.src=b.textContent}); - function css(v){return getComputedStyle(document.documentElement).getPropertyValue(v).trim()} - function draw(){ - blocks.forEach(function(b){b.removeAttribute("data-processed");b.textContent=b.dataset.src}); - mermaid.initialize({startOnLoad:false,look:"handDrawn",theme:"base",securityLevel:"strict", - fontFamily:"Kalam, var(--font)",flowchart:{curve:"basis"}, - themeVariables:{fontFamily:"Kalam, cursive",fontSize:"17px",background:css("--surface"), - primaryColor:css("--card"),primaryBorderColor:css("--primary"),primaryTextColor:css("--fg"), - secondaryColor:css("--brand-soft"),tertiaryColor:css("--surface"), - lineColor:css("--muted"),textColor:css("--fg"),edgeLabelBackground:css("--surface")}}); - mermaid.run({nodes:blocks}).catch(function(){})} - draw();document.addEventListener("themechange",draw); - matchMedia("(prefers-color-scheme: dark)").addEventListener("change",draw); -})(); -""" - - -def main(): - out = Path(sys.argv[1] if len(sys.argv) > 1 else "_site") - root = Path(sys.argv[2]).resolve() if len(sys.argv) > 2 else Path(__file__).resolve().parent.parent - market, plugins = load_plugins(root) - if not plugins: - sys.exit("no plugins found under %s/plugins" % root) - (out / "assets").mkdir(parents=True, exist_ok=True) - (out / "changelog").mkdir(exist_ok=True) - (out / "index.html").write_text(render_home(root, market, plugins), encoding="utf-8") - for p in plugins: - (out / p["dir"]).mkdir(exist_ok=True) - (out / p["dir"] / "index.html").write_text(render_plugin(root, p), encoding="utf-8") - (out / "changelog" / "index.html").write_text(render_changelog(root, plugins), encoding="utf-8") - (out / "feed.xml").write_text(render_feed(plugins), encoding="utf-8") - for a in (root / "site/assets").iterdir(): - (out / "assets" / a.name).write_bytes(a.read_bytes()) - (out / ".nojekyll").write_text("") - print("wrote %s (%d plugin page(s), %d release(s))" - % (out, len(plugins), sum(len(p["releases"]) for p in plugins))) - - -if __name__ == "__main__": - main() diff --git a/site/assets/linearb-logo.svg b/site/assets/linearb-logo.svg deleted file mode 100644 index fb60116..0000000 --- a/site/assets/linearb-logo.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/site/assets/linearb-mark.svg b/site/assets/linearb-mark.svg deleted file mode 100644 index ea3b5a4..0000000 --- a/site/assets/linearb-mark.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/tests/site/build_test.sh b/tests/site/build_test.sh deleted file mode 100755 index 80cccb0..0000000 --- a/tests/site/build_test.sh +++ /dev/null @@ -1,107 +0,0 @@ -#!/usr/bin/env bash -# Site: builds from the real repo, every plugin gets a page and every release shows up, a new plugin -# is picked up with no code change, and markup in READMEs/CHANGELOGs is escaped. -set -uo pipefail - -ROOT="$(cd "$(dirname "$0")/../.." && pwd)" -BUILD="$ROOT/scripts/build-site.py" -pass=0; fail=0 -check() { - local name=$1; shift - if "$@"; then pass=$((pass+1)); printf 'ok %s\n' "$name"; else fail=$((fail+1)); printf 'FAIL %s\n' "$name"; fi -} -has() { grep -qF -- "$2" "$1"; } -lacks() { ! has "$1" "$2"; } - -W="$(mktemp -d)"; trap 'rm -rf "$W"' EXIT - -# The real repo. -"$BUILD" "$W/site" >/dev/null -for f in index.html changelog/index.html feed.xml assets/linearb-logo.svg .nojekyll; do - check "built $f" test -e "$W/site/$f" -done -check "feed.xml is valid XML" python3 -c "import sys,xml.dom.minidom; xml.dom.minidom.parse(sys.argv[1])" "$W/site/feed.xml" -for src in $(jq -r '.plugins[].source' "$ROOT/.claude-plugin/marketplace.json"); do - d="$ROOT/${src#./}"; name="$(basename "$d")" - check "$name has a page" test -s "$W/site/$name/index.html" - check "$name is on the home page" has "$W/site/index.html" "href=\"$name/\"" - check "$name managed settings enable it" has "$W/site/$name/index.html" ""$name@linearb-ai": true" - while read -r v; do - check "$name $v in the changelog" has "$W/site/changelog/index.html" "id=\"$name-v${v//./-}\"" - done < <(sed -n 's/^## \[\([^]]*\)\].*/\1/p' "$d/CHANGELOG.md") -done -check "install popover has the one-line command" has "$W/site/agentic-advisor/index.html" 'claude plugin marketplace add linear-b/agent-plugins && claude plugin install agentic-advisor@linearb-ai' -check "install popover has the Claude prompt" has "$W/site/agentic-advisor/index.html" 'Install agentic-advisor for me' -check "README diagram inlined with a phone layout" has "$W/site/agentic-advisor/index.html" '
    ' -check "diagram follows the site theme" has "$W/site/agentic-advisor/index.html" 'var(--brand-soft,#eef0ff)' -check "README #anchors kept" has "$W/site/agentic-advisor/index.html" 'id="usage-telemetry-on-by-default"' - -# A plugin added to the marketplace appears with no code change; one without a CHANGELOG still builds; -# a folder the marketplace doesn't publish gets no page (its install command wouldn't work). -F="$W/repo"; mkdir -p "$F" -cp -R "$ROOT/.claude-plugin" "$ROOT/plugins" "$ROOT/site" "$F/" -mkdir -p "$F/plugins/demo-tool/.claude-plugin" "$F/plugins/bare/.claude-plugin" -printf '{"name":"demo-tool","version":"0.3.0","description":"Demo plugin."}\n' > "$F/plugins/demo-tool/.claude-plugin/plugin.json" -cat > "$F/plugins/demo-tool/README.md" <<'MD' -# demo-tool - -Does a demo thing . - -## Setup - -Run `demo ` and see [docs](docs/guide.md), [shared](../shared.md), [root](/SECURITY.md) or [bad](javascript:alert(1)). - -## Setup - -Literal {{CSS}} stays text. - -![flow](docs/flow.svg) - -![evil](docs/evil.svg) - -```mermaid -flowchart LR - A --> B -``` - -| a | b | -| --- | --- | -| 1 | | -MD -mkdir -p "$F/plugins/demo-tool/docs" -printf '\n' > "$F/plugins/demo-tool/docs/flow.svg" -printf '\n' > "$F/plugins/demo-tool/docs/evil.svg" -printf '# Changelog\n\n## [0.3.0] - 2026-10-01\n\n### Added\n\n- First release ([#1](https://github.com/o/r/pull/1))\n' > "$F/plugins/demo-tool/CHANGELOG.md" -printf '{"name":"bare","version":"0.0.1"}\n' > "$F/plugins/bare/.claude-plugin/plugin.json" -mkdir -p "$F/plugins/unlisted/.claude-plugin" -printf '{"name":"unlisted","version":"9.9.9"}\n' > "$F/plugins/unlisted/.claude-plugin/plugin.json" -jq '.plugins += [{"name":"demo-tool","source":"./plugins/demo-tool"},{"name":"bare","source":"./plugins/bare"}]' \ - "$ROOT/.claude-plugin/marketplace.json" > "$F/.claude-plugin/marketplace.json" -"$BUILD" "$W/site2" "$F" >/dev/null -P="$W/site2/demo-tool/index.html" -check "new plugin gets a page" test -s "$P" -check "new plugin on the home page" has "$W/site2/index.html" 'href="demo-tool/"' -check "new plugin in the changelog" has "$W/site2/changelog/index.html" 'id="demo-tool-v0-3-0"' -check "new plugin in home managed settings" has "$W/site2/index.html" '"demo-tool@linearb-ai": true' -check "plugin without README/CHANGELOG still builds" test -s "$W/site2/bare/index.html" -check "no changelog link for a plugin without releases" lacks "$W/site2/bare/index.html" 'changelog/#bare' -check "no home changelog button for a plugin without releases" lacks "$W/site2/index.html" 'changelog/#bare' -check "README raw html escaped" lacks "$P" '