37 Commits
Author SHA1 Message Date
Kevin Codex 1f25242207 chore(hosts): move the explorer to explorer.twigpine.com
The explorer, its docs and its agent files now name explorer.twigpine.com
and node / node2 / node3.twigpine.com, and the proxy rules call the nodes
by those names. explorer.gitlawb.com forwards pages to the new address;
the API proxy, llms.txt, skill.md and the raw docs keep answering there.
The Manila node keeps its current address, and the documented node default
for ICAPTCHA_URL is unchanged because it is the value the node ships with.
2026-10-13 15:51:30 +08:00
Kevin Codex de3c84f60e docs(agents): install from twigpine.com and name the twigpine tap
- The curl installer is now https://twigpine.com/install.sh with -fsSL.
  That copy only installs archives listed in twigpine.com's checksum list;
  gitlawb.com serves an older pinned deployment and may redirect later,
  and `curl -sSf` without -L pipes a redirect's "Redirecting..." into sh.
- brew install twigpine/tap/gl instead of `brew install gl`, which is
  ambiguous while gitlawb/tap is still tapped. skill.md also tells agents
  how to drop the old tap.
2026-09-30 14:54:56 +08:00
Kevin Codex 7081d8ee2d docs(agents): point install, tap and image refs at Twigpine
The GitHub org moved to Twigpine and github.com/gitlawb was re-registered by
someone else on 2026-09-29. Agents fetch public/skill.md (also proxied at
twigpine.com/skill.md) and run its commands as written, so these become code
execution the moment that org creates the matching repo or image:

- `brew tap gitlawb/tap && brew trust gitlawb/tap` (skill.md,
  docs/agents.md, docs/quickstart.md) -> twigpine/tap. A tap runs Ruby
  from the tapped repo on every install and upgrade.
- `docker run ... ghcr.io/gitlawb/node:latest` (docs/node.md) ->
  ghcr.io/twigpine/node. GHCR has no rename redirect, and the image already
  lives under twigpine; the old path fails today and is claimable.
- Source, release and clone links (skill.md, llms.txt, docs, README,
  CONTRIBUTING, package.json) -> github.com/Twigpine.
2026-09-30 14:12:46 +08:00
Kevin Codex 3b54afb8fd docs: storage section reflects per-node Postgres + object store (no IPFS/Filecoin/Arweave) 2026-08-27 16:48:49 +08:00
Kevin Codex 34b33c1f5b docs(legal): token-free run-a-node guide; drop staking/rewards/escrow copy from llms.txt, skill.md, docs, home tile 2026-08-27 13:25:01 +08:00
Vasanth T f4af95d034 Mobile: fix content running off the side, and rework the navigation as a drawer (#12)
* fix(mobile): show the node status text, and stop content running off the side

A tester on Safari/iOS reported a green dot on the overview with nothing beside
it, and asked whether the dot was intentional. It was not the dot that was
wrong — the words next to it were missing.

That line was rendered with AnimatedGradientText, which paints text through
`bg-clip-text` over a transparent colour, so the words exist only as a mask on
a gradient. The dot is a real fill and survived; the status and the version went
with the gradient. Whether the node is reachable and which version it runs is
the page's first factual claim, and it should not depend on a paint effect, so
it is plain text now. This is the same failure AuroraText had in light mode:
text that is invisible when its background does not render, with nothing in the
markup to indicate anything is wrong.

Two layout faults at 375px, both pushing content past the right edge where it
could not be reached:

- AgentSurface sat in a grid whose items default to `min-width: auto`, so a
  46ch paragraph — 386px against 343px of available width — widened the track
  and carried the rows with it. The copy buttons ended up at x=441 on a 375px
  screen. The tracks may shrink now and the prose is capped to the viewport
  below sm.
- The icon cloud canvas carries width/height attributes of 400, which size the
  drawing buffer rather than the box, so it laid out at a fixed 400px. It is
  capped with max-w-full; the buffer stays 400x400, so nothing about the
  rendering changes. Measured after: 343px on a 375px screen.

Unclipped overflow across the eight routes went from 22 elements to 0.

Filter controls were 28px tall, which is an uncomfortable thumb target; they
are 36px below sm and unchanged from sm up. The segmented indicator measures
both axes already, so it follows the taller row without further work.

Worth recording that the page never actually scrolled sideways: the preview
pane reports an inflated window.innerWidth, and comparing element rects against
it invents overflow that is not there. The figures above come from comparing
rects to the body's own box, where the scaling cancels out.

* fix(mobile): keep the current tab and document visible, and size header targets

Two scrolling strips hid the very thing they were meant to show.

The repository tab strip scrolls once its tabs outrun their box, which on a
375px screen is 440px of tabs in 343px. Arriving on ?tab=events left the strip
at scrollLeft 0 with the selected tab past the right edge, so the reader saw
the first tabs and nothing indicating which one they were on. The strip now
reveals its selected tab. It re-runs on resize as well, because each tab's
count arrives from its own fetch — a tab grows from "certs" to "certs 1" a
moment after selection, which was enough to push a tab revealed on the first
pass back over the edge. Verified across all six tabs.

The docs section nav has the same shape below lg: four labels come to 419px in
351px, so /docs/node opened with "Run a node" out of sight. Same treatment.

Both scroll only their own container. scrollIntoView would have moved the
document vertically too and thrown the reader past the header they had just
landed on; the page scroll position is unchanged through every case.

Header targets on phones:

- The wordmark was `hidden sm:inline`, leaving the home link as a 15x18px
  glyph. The bar has roughly 200px of empty spacer at 375px, so the wordmark
  now shows at every width — it identifies the site and gives the link a real
  target.
- The palette, theme and menu buttons were 32px; they are 40px below sm.
- Documentation nav links were 34px; they are 40px below sm.

Desktop is untouched: segmented controls stay 28px, the sort trigger 32px, and
the header buttons 32px from sm up.

Unclipped overflow is 0 across all eight routes at 375px.

* refine(mobile): compose the hero and the repository row for a phone

Until now the mobile work had been fixing faults — overflow, tap targets,
strips that hid the item they were meant to show. This is the design pass.

The hero was 918px tall on a 375x812 screen, so the whole first screen was
hero and nothing of the product showed until the reader scrolled. Inside it the
display line wrapped to three ragged lines at 40px, and the two calls to action
stacked at 199px and 273px, which reads as an accident rather than a pair.

- 32px display type below sm sets the line in two, not three.
- The lede drops to 15px with tighter leading. Geist Mono is a wide face and
  seven lines of it is a wall on a phone.
- Both buttons run full width and share an edge.
- Hero padding and the gap above the counters come in.

The hero is 791px now and the live counters reach the fold, so the first screen
carries a real figure instead of ending on marketing.

The repository row was a two-column grid at every width. On a phone the meta
column took 146px of a 343px row, leaving the name and description to share
149px. Stacked below sm, the description gets 311px and runs to two lines
rather than being cut mid-word; desktop keeps one line and the side-by-side
layout, where density is the point.

The clone-copy control was revealed on hover. A touch screen has no hover, so
on a phone it could not be reached at all. It is always visible below sm.

Not done, and worth a decision: the lede is still seven lines on a phone. The
drop to 15px bought height, not lines — the sentence is simply long for a
343px measure in a monospace face. Shortening it is a copy change rather than
a layout one, so it is left alone.

* feat(mobile): make the navigation a drawer

The menu was a panel that animated its height open beneath the header. It
worked, but it behaved like page content rather than a layer over it: it pushed
the page down, it could not be dismissed by dragging or by tapping away, and it
left the page behind fully lit with nothing marking it as inactive.

It is a bottom sheet now, built on Vaul — a drawer on top of Radix Dialog, so
the behaviour a menu needs comes with the library rather than being
reimplemented: focus trapped and restored, Escape to close, the page behind
inert and locked against scrolling, and drag-to-dismiss.

It opens from the bottom because that is where a thumb already rests on a tall
phone; the top of an 812px screen is the hardest part of it to reach. Rows are
48px, the active section is filled and marked with aria-current and a check,
and a grabber shows the sheet can be thrown shut. The node's own figures and
the llms.txt / skill.md links ride along at the foot.

autoFocus is set explicitly. Vaul leaves it off by default, which left focus on
<body> after opening: a keyboard or screen-reader user opened the menu and was
still outside it, with the next Tab starting from the top of the document
behind the overlay.

Measured at rest with transitions suppressed, since this environment does not
advance them: the sheet sits at top 252 of an 812px screen, flush to the bottom
edge, all eight links reachable, focus inside, Escape closing it and restoring
body scroll. Desktop is untouched — the trigger stays md:hidden and the
eight-link bar is unchanged.

* fix(network): the federation map ran off the side of a phone

The peer chips were cut 67-75px past the right edge, so node2, node3 and manila
were unreadable on a 375px screen. The map is a two-column arrangement — this
node on the left, its peers on the right — and two columns of ~130px chips plus
a 32px gap do not fit in 343px of content width.

Peers now show the first label of the host below sm, which is the part that
distinguishes them; the full host was already in each chip's title and still
shows from sm up. Column and row gaps come in on a phone. Every chip now ends
at 359px against a 375px screen.

The globe is capped at 250px below sm. It is decorative — the markers are a
stock world spread, not peer positions, as the component has always said — and
at 343px square it claimed most of a screen, nearly all of it dark sphere
against a dark ground, which is the emptiness that made the section look wrong.
The drawing buffer is untouched, so nothing about the render changes. The
section is 57% of a phone screen now rather than 69%.

Also worth recording: the earlier overflow sweep reported this page clean
because it discounted anything an ancestor clipped, on the grounds that clipped
content cannot scroll the page sideways. That was the wrong test — clipped
content is still content the reader cannot see. Counting what sticks out
regardless of clipping is what found this.

* fix(a11y): layer the default focus ring so components can style their own

The global :focus-visible rule sat outside any cascade layer, and unlayered CSS
beats layered CSS regardless of specificity. Every Tailwind focus utility in the
codebase therefore lost to it, and each control got the default 2px accent
outline inset by a pixel instead of the treatment it declared.

The command palette showed it worst: opening with the shortcut is
keyboard-initiated, so :focus-visible matched the autofocused search field and
drew a hard rectangle around an input that sits flush inside a rounded panel and
already indicates focus with its caret. It was visible on both desktop and
mobile because it had nothing to do with viewport size.

This is the second time this file has hit that rule — the border-color reset
seventy lines above carries a comment warning about exactly it.

Moving the ring into @layer base keeps it as the default for anything that does
not style its own focus, while a component that does now wins. Verified in the
built stylesheet: the base ring compiles at offset 8148 inside @layer base and
focus:outline-none at 55995 inside @layer utilities, so the override applies.

Inputs are not left without an indicator — they turn their underline to the
foreground on focus, and the caret is visible. Buttons and tabs keep the
focus-visible outlines they declare themselves.
2026-08-19 20:05:43 +08:00
Kevin Codex 2641fe6f93 fix: make repository file viewing reliable 2026-08-19 15:37:36 +08:00
Vasanth T 627680fbe7 Explorer redesign: identity, control system, docs, and search (#11)
* chore: ignore the impeccable hook session cache

hook.cache.json records per-session edit counts keyed by absolute paths on the
machine that produced it, so it is local state rather than project config.
config.json, which carries the shared detector settings, stays tracked.

* chore(deps): add the redesign's UI dependencies and fix module resolution

Adds @radix-ui/react-select (the sort menus), tw-animate-css (the enter/exit
utilities Radix drives from data-[state]), github-slugger (heading anchors for
docs search), motion, gsap, cobe, and the Magic UI / Aceternity runtime deps.

tsconfig gains baseUrl and the "@/*" path mapping. Without it the shadcn CLI
resolved "@/lib/utils" as a literal directory and wrote vendored components
into a folder named "@" at the repo root.

eslint.config.js exempts the vendored registry components from
react-hooks/purity and react-hooks/set-state-in-effect. Those files are
generated and `shadcn add` overwrites them, so the violations cannot be fixed
in place.

* feat(ui): vendor the Magic UI and Aceternity components

Installed through the shadcn CLI from the Magic UI and Aceternity registries.
They are checked in rather than depended on, which is how those registries are
meant to be used: the source is the deliverable.

Two fixes applied on top of what the registry shipped, both silent failures:

- The pulse keyframes declared `boxShadow:` — a JS property name in a CSS
  block. Browsers dropped the declaration, so PulsatingButton's glow did
  nothing. Four declarations corrected to `box-shadow`.
- ScrollProgress hardcoded a purple/pink/orange gradient. This palette is
  monochrome and inverts with the theme, so the bar now paints the foreground.

* feat(theme): rebuild the token system on gitlawb.com's identity

Colours are extracted from the live gitlawb.com rather than approximated: pure
#000 ground, pure #fff text, Geist Mono throughout. The over-scoped theme
customizer is removed in favour of a single dark/light toggle.

Notable fixes folded in here, each of which broke something silently:

- The `*` border-color reset sat outside any cascade layer, so it beat every
  layered border-color utility regardless of specificity. `border-success`,
  `border-danger`, and `border-transparent` all painted the same faint white.
  Moved into @layer base.
- Syntax-highlight .tok-* colours existed only for dark, leaving near-white
  glyphs on the light ground. Both sets now defined.
- The light palette had been mirrored from dark, but alpha compositing is not
  symmetric: black at 45% over white is 1.75:1 where white at 45% over black
  is 10:1. The light ramp is retuned, and --success moved to #00712f, which
  clears 4.5:1 on both the page and its own tint.
- AuroraText and AnimatedGradientText painted through background-clip with a
  hardcoded ramp, so they rendered invisible in light mode with nothing for a
  contrast check to see. The ramp is now themed.

Radius: rules and list hairlines stay square, controls take a full round to
match the search field, and floating panels take 12px.

useIsDark subscribes to the class on <html> via useSyncExternalStore, because
canvas components (the globe, the icon cloud) bake colours into textures and
image URLs and must re-render on a theme flip. useTheme cannot serve that — it
holds its own state per call site, so a second caller never hears the toggle.

useTheme also forces a full style recalculation after the flip. Chromium does
not reliably invalidate `color` on existing elements when an inherited custom
property changes on the root: measured after one toggle, 76 of 236 .text-muted
elements kept the previous theme's colour while a freshly created element with
the same class in the same parent resolved correctly.

* feat(ui): frameless control kit, and drop the native select

At zero radius a bordered control reads as a hard box, and a listing page puts
hundreds of them on screen at once. Buttons are frameless by default, fields
are ruled with a single underline, and a frame or a fill is reserved for the
control that has to stand apart. SearchField defines the one search shape every
listing page uses, so there are no longer four slightly different ones.

Dropdown replaces the native <select>. A native select paints its option list
as an operating-system popup that lives outside the document: it cannot take
this page's colours, its radius, or its type, and on Windows it draws as a
grey box. Styling the closed control did nothing to the opened one. Built on
Radix Select, which renders the list in the page while keeping the keyboard
and screen-reader behaviour the native control had.

SegmentedControl replaces loose pill rows for single-choice filters. One track,
one indicator that slides between options, positioned from the active button's
measured box and animated with a CSS transition. Motion's layoutId is the
idiomatic way to write this, but the measured version's correctness is
checkable without watching an animation, and it does not depend on Motion's
projection surviving the router transition a filter click schedules.

Also fixes every `rounded-[--radius]` in the kit. That is Tailwind v3 syntax
for a CSS variable; v4 requires `rounded-[var(--radius)]`. The class compiled
to nothing, so these controls had been drawing square no matter what the token
said.

* feat(layout): rebuild the top bar, footer, and overview

The nav rail is a single Motion element shared across links via layoutId, so
it slides between items rather than cross-fading. The palette trigger is
icon-only with the shortcut in its tooltip.

Adds lib/platform.ts. The palette handler accepts metaKey || ctrlKey, but the
UI hardcoded the Mac glyph, so Windows and Linux visitors were told to press a
key their keyboard does not have. modifierKeyLabel() reports the right one.

* feat(repos): rebuild the repository index

One toolbar row: the shared search field, a sort dropdown, and the fork filter
as a segmented control. Rows are denser and the constant-value pills that
carried no signal are gone.

The fork filter runs over the repos already fetched rather than the whole node,
so while the list is paginated the scope is stated. Without it, filtering to
"forks" answered "no repositories match" having examined 50 of 3,207 — which
reads as "this node has no forks".

timeAgo clamps at zero. The node's clock can sit ahead of the browser's, which
produced timestamps like "-28s ago" on freshly pushed repos; under five seconds
now reads "just now". Two tests cover it.

* feat(repo-detail): rebuild the repository detail page

Removes StatsPanel, whose figures duplicated the header. The file tree, commit
list, certificates, issues, and pulls move onto the shared control kit.

Fixes a deep-link hang: opening ?tab=certs directly left the tab on "loading…"
forever. The ref marking a tab as started survives StrictMode's simulated
remount, while the cleanup aborts the in-flight fetch — so the retry was
skipped because the marker said the work was already underway. The marker is
now cleared when its request aborts.

* feat(agents): trust sorting, band filters, and the ecosystem cloud

Adds four sorts and a trust-band segmented filter. Band counts are computed
after the search but before the band filter, so each band reports what it would
yield rather than collapsing to the active one.

AgentRow drops the gauge and the tier colours for a text readout, and prints a
status chip only when the status departs from active — at 4,718 agents the
decoration was the majority of the ink.

AgentEcosystem is an icon cloud of the tooling that can drive this network.
Its heading is "Works with" deliberately: the agents API returns did:key
identities with no vendor field, so nothing here claims which of these have
agents registered. Mark colours are part of the CDN request URL, so they are
re-fetched per theme — white logos are invisible on the light ground.

* feat(network): globe, federation map, and peer orbit

The globe is deliberately illustrative. The node API exposes no geography — a
peer record is { did, http_url, last_seen, reachable } and nothing more — so
the markers are a stock world spread, not peer locations. A map pin gets read
as a claim about where a machine is, so the real federation view stays in the
topology diagram and the node cards below, which are driven by reachability.

Its colours are painted into a canvas and cannot come from CSS tokens, so they
switch on the theme here: a dark sphere on the light ground reads as a hole in
the page. The component replaces cobe's default config wholesale rather than
merging, so every field has to be supplied — the stock default ships ten
hardcoded demo cities.

* feat(events,tasks): rebuild the feeds

Events moves to the shared SearchField; it was the last page hand-assembling
its own search input, a near-verbatim copy that would have drifted the moment
SearchField changed. The task status filter becomes a segmented control, with
each status keeping its colour dot — SegmentedOption gained an icon slot for
it, and the indicator now tracks both axes so a long filter row can wrap
without the indicator landing on the wrong line.

* feat(docs): rebuild the documentation page

Three columns: a sticky section nav, the article, and the scroll-spy TOC. The
old layout capped the prose at 70ch and left the right half of the screen
empty. Every document opened with its title twice — once from the page header
and once from the markdown h1 — so the leading h1 is dropped.

Each fence gains a language label and a copy button. Section headings gain
anchors. A prev/next pager closes each page, since the four documents are meant
to be read in order.

"Read this as text" is now on the page at every width. It was hidden below the
lg breakpoint, so the single thing that distinguishes these docs — that an
agent can fetch the same words as raw markdown — was invisible on anything
narrower than a laptop.

Heading ids are now a pure function of the markdown. gfmHeadingId keeps one
slugger on the shared engine and its de-duplication counter carries across
parse calls, and engine.parse awaits Shiki for every fence, so two renders
started close together interleave and feed the same slugger. StrictMode starts
the fetch twice, so this was the normal case: ids came out `prerequisites-1`,
then `-2`. Every deep link and every TOC anchor depended on how many times a
document had been rendered that session. Parses are now serialized, and reset
between.

* feat(docs): search the documentation from the command palette

The four documents are fetched once on first open and split at their headings,
so a result lands on the paragraph that matched rather than the top of a page.
Heading matches outrank body matches; fenced commands are indexed, so
searching "gitlawb/gl" finds the install block. Anchors come from
github-slugger, the same slugger the renderer uses — deriving the rule by hand
would drift and produce links to anchors that do not exist.

The parser splits on either line ending. These files are checked out CRLF, and
splitting on "\n" alone leaves a trailing "\r" that the heading pattern can
never match, because `.` in JavaScript excludes carriage return as a line
terminator and `(.*)$` therefore stops short of the end of the string. Every
heading silently failed and each document indexed as one undivided section.

The palette itself takes the radius tokens and a per-kind mark, so a row's
target is legible before the label is read. Its input is now a proper combobox
with aria-controls and aria-activedescendant, and section headings are siblings
of the rows rather than nested inside them — they had been announced as part of
the first result.

* fix(api): never serve the docs hub index in place of a document

vercel.json has no path that 404s: /docs/:slug rewrites into this function and
:slug matches a segment containing a dot, while /(.*) catches everything else
and serves index.html. So when public/docs/<slug>.md missed the filesystem the
subrequest did not fail — it was rewritten back into the same function, which
answered 200 with the hub index because the subrequest's Accept is not html.
That was then served as the requested document, with Content-Type text/markdown
and an hour of CDN caching. response.ok was always true, so the 502 fallback
below it was unreachable.

Two guards. The subrequest carries x-gitlawb-doc-markdown; reaching the handler
with that header means the static file was absent, because a file that exists
is served before rewrites are consulted, so it answers 404 at the first hop.
And the fetched body is rejected if it is the SPA shell or contains the hub's
marker line — fetchIndexHtml already guards its own fetch this way, by testing
the body rather than trusting the status.

A listed slug whose file is missing is a broken deploy, not a passing outage,
so it now returns an uncached 404 rather than a cached 502 claiming to be
temporary.

The test stub modelled the deployment wrongly, which is why this survived: it
returned 404 for absent files, under a comment claiming to reproduce
deployment behaviour. It now performs the actual rewrite. Verified by
disabling both guards — exactly the four new tests fail, the eleven existing
ones pass.

* docs: correct three claims contradicted by this repo's own code

quickstart.md and protocol.md both told the reader to verify a push on the
global /events feed. That feed carries only ref updates gossiped in from peer
nodes; a push this node received directly is recorded as a per-repo signed
certificate and is absent from the feed's data entirely. This is the failure
commit 8e941ee removed from the UI, still present in the prose — and in
quickstart it lands at the worst moment, immediately after a first-time
reader's first push, where an empty feed reads as a failed write. Both now
point at the repository's certificates, which is what agents.md already said.

agents.md listed "clone fails on https:// URL | wrong scheme | clone URLs must
be gitlawb://". The node serves standard git smart-HTTP at the https clone_url,
and protocol.md documents that transport. An agent hitting the most common
documented failure — git-remote-gitlawb missing from PATH — was being steered
away from the one fallback that works.

* docs: record the product truth and the design-detector exemption

PRODUCT.md carries what the code cannot: who the surface is for (developers
and agents evaluating the network), the visual register, the directions that
were tried and rejected, and that Aceternity, Magic UI and GSAP are to be used
as real components rather than reimplemented by hand.

The detector exemption records why "geist mono" is not an overused-font finding
here: the owner requires the explorer to match gitlawb.com, whose live site
computes Geist Mono on body and h1.

* feat(home): close out the overview instead of letting it run out

The page ended wherever the activity list happened to stop. Measured: the
sidebar's content came to 936px against the main column's 532px, and because
both grid columns stretch to the taller one, that left 387px of dead space
under the wide column — the page read as truncated rather than finished.

Three additions, and one of them is placed to fix that directly.

"Repositories on this node" goes in the main column, not below the grid, where
it would have left the hole untouched. Six cards two-up come to roughly the
height that was missing, so the columns now balance at 1061 against 936 and
the remainder falls under the narrow column where it barely reads. The page
had been closing on ref-update lines, which say the network is moving but not
what is on it.

Every field on a card is read from the repository record as the node returns
it: star_count and forked_from are real columns, so the star count and the
fork mark are facts, and both are omitted when they carry nothing. Order is
whatever the node returns — fetchRepos omits the sort param when the sort is
'updated', so nothing claims these are the most recently touched, and the hook
now records that so no future heading asserts it.

"Built for agents" states the machine-readable surface on the front page:
llms.txt, skill.md, and docs/agents.md, each a real file served from this
origin with a copy button handing over the absolute URL an agent needs. This
is the claim the product is built on, and it was only reachable through /docs
— the overview sold the explorer as a dashboard and said nothing about the
part that is unusual.

"Start here" is the closing beat: publish, operate, understand. Every
destination is a route declared in App.tsx.

CopyButton gains srLabel. Its `label` was both the visible text and the
accessible name, so a descriptive name rendered in full as button text. Three
copy buttons in one list now announce which file each belongs to instead of
"copy Copy" three times over, and no existing call site changes visually.

* fix(home): say that the overview's event feed is gossip-only

Both sections read /events/ref-updates, which carries only ref updates
gossiped in from peer nodes. A push this node received directly is recorded as
a per-repo signed certificate and never enters that feed.

Measured against a live node: all 50 events returned had from_peer set, the
newest was 2,919 minutes old, and all six repositories rendered further down
the same page had been updated within the previous 51 minutes. Headed "Latest
ref-update" and "Recent activity" with no qualifier, an actively used node
reads as one that stopped two days ago.

The data was right; the labels were not. They now say what the feed is, in the
same terms /events already uses, and the list states where local pushes are
recorded instead. The ordering and de-duplication were checked at the same
time and are correct: the API returns newest-first, and the first distinct
event is the one printed above the list, so slicing from index 1 skips exactly
the row already shown.

* fix(home): stop the gossip diagram asserting links that do not exist

The P2P section drew animated beams from this node to three named peers taken
from `peers.filter(p => p.reachable)` — HTTP reachability from GET /peers,
which says nothing about libp2p connectivity. Two lines below, the same
section printed the real figures. Measured on a live node: 16 of 75 peers
answer over HTTP, connected_peers 0, gossipsub_mesh_peers 0. The picture
claimed a live mesh while the numbers in the same box denied it.

Beams are now drawn from connected_peers, so none appear when nothing is
connected. They are unlabelled, because /p2p/info reports how many peers are
connected and not which — naming them means borrowing identities from the HTTP
list and asserting a link the API never made. An empty mesh is stated as the
ordinary condition it is, and the HTTP-reachable count is kept as its own
separate figure rather than being quietly used to stand in for gossip.

Clone a repository now names what it clones. The command is real, but it wraps
a ~60-character did:key remote over three lines, so the repository was
unreadable, and the button underneath offered to copy a command for something
the reader could not identify.

Agent tasks gains a proportion bar. This register is overwhelmingly one status
— 198 of 200 completed here — and four figures in a row read as four
comparable quantities. Widths come from the same counts as the numbers, so the
two cannot disagree.

The bar's fills come from a new taskStatusFill rather than from
taskStatusColor with the prefix swapped at runtime. Tailwind generates CSS
only for class names it can read in the source, so "text-success".replace(...)
would have produced no rule and painted an invisible bar.

* fix(repo-detail): tighten the file list to scanning density

Rows carried 24px of vertical padding and came out 47px tall, so a twelve-entry
tree ran past a screen and reading a repository's contents meant scrolling.
A file list is for scanning; the row is now sized to its content at 33px, close
to what a forge uses. On a 168-entry tree that takes the list from 7,932px to
5,579px.

Two smaller corrections in the same pass:

The size column was w-20, too narrow for "32.2 KB", so the unit wrapped under
the number and that row alone stood 4px taller than its neighbours. It is now
w-24/w-28 with the value held on one line.

Both cells carry a fixed 20px line box. The size column is set in mono, whose
taller line box would otherwise make every row that carries a size deeper than
the directory rows that carry none. Rows now measure uniformly.

* fix(ui): NumberTicker rendered 0 until its animation ran

The component rendered `startValue` as its DOM text and only ever wrote the
real figure from the spring's change handler, which is gated behind useInView.
Any statistic below the fold therefore displayed "0" for a value that was
already loaded — on the network page, "0 repos" for a node holding 2,120 —
until the reader happened to scroll it into view, and permanently anywhere the
animation could not run.

Zero is not a placeholder. NodeCard prints "—" when a figure is genuinely
missing, so a rendered 0 reads as a measured fact, and it was false.

The span now renders the true value. When the animation runs its first frame
overwrites this within a tick and the count-up is unchanged; when it does not,
the number is simply correct. Verified with animations suppressed: the four
node cards render 3,208 / 4,718 / 51,451 and 2,120 / 190 / 65, matching
/api/v1/stats and /nodes/node2/api/v1/stats exactly, where previously every
figure rendered 0.

Vendored from the Magic UI registry — `shadcn add number-ticker` overwrites it.

* fix(nav): reset scroll when the page changes

A browser scrolls to the top on a real navigation; a single-page app does not,
because the document never changes. Nothing in this app compensated, so
following the "Next" pager at the foot of a document loaded the next one and
left the reader parked at its bottom, looking at the pager again — the content
had changed but the viewport had not moved. Every cross-page link behaved the
same way; the pager only made it obvious, because that control sits at the very
end of the page.

ScrollToTop keys on the pathname and leaves three cases alone:

- Back and forward. POP means the browser is restoring a position the visitor
  already had, and overriding that loses their place.
- Anything carrying a hash, which names a heading to land on. The page owning
  that anchor scrolls to it once its content has rendered.
- Query-string changes. Search, filters, sorting and paging rewrite the query
  while staying on the same page, and resetting there would yank the view to the
  top on every keystroke typed into a search field.

Verified: pager 2707 to 0, sidebar link 1200 to 0, back restores 2707, and
typing into the repository search at scroll 600 leaves it at 623 on a page
still 3367 tall.

Two fixes to the docs anchor scroll, which this exposed.

The hash now comes from the router and is a dependency. Reading
window.location.hash and keying only on the slug meant a jump into a document
already open changed nothing the effect watched, so following a second search
result within the same page did nothing at all.

And the scroll no longer waits for requestAnimationFrame. DocArticle rewrites
that markup in a layout effect, and a child layout effect runs before this
parent effect, so the heading already sits at its final offset. The frame only
bought a dependency on the tab being painted: opened in a background tab, the
callback was deferred and the reader arrived at the top.
2026-08-19 14:06:23 +08:00
Kevin Codex 23686417dc docs: bless the cautious-agent install path + HTTP push verification
Feedback from an agent-driven smoke run: careful agents refuse piped
installers and unpinned globals, and want to verify a write without
trusting the CLI or a web page. Document the pinned and throwaway-prefix
npm installs as first-class, state explicitly that the write loop needs
no wallet key and no config edits (ETH_PRIVATE_KEY is name-registry
only), and add a 'verify your push over plain HTTP' section covering
the per-repo events and certs endpoints — including the caveat that the
global explorer feed is gossip-only. Skill bumped to 2.2.

The docs OG card advertised 'curl explorer.gitlawb.com/docs', which
returns an empty 308 without -L; the card now shows 'curl -sL …'.
2026-08-17 16:12:08 +08:00
Kevin Codex 8e941ee1d7 fix: stop presenting the gossip-only events feed as containing local pushes
The node's global ref-updates feed only carries events gossiped in from
peers (received_ref_updates has from_peer NOT NULL); pushes received
directly by this node are recorded as per-repo signed certificates and
never appear there. The events page claimed otherwise: a permanent
'local: 0' stat, a local/gossip filter that could never match, an empty
state promising 'push a repo to this node to see activity', and a
search that came up empty for freshly pushed repos — reading as a
failed write to anyone verifying a push.

Describe the feed as peer gossip, replace the dead local stat and
source filter with a distinct-peer-nodes count, and add a local pushes
panel: searching now also matches repos by name/owner/description and
shows their signed push certificates from the per-repo events feed,
with links to the repo page.
2026-08-17 16:12:00 +08:00
Kevin Codex 2d873b3914 feat: serve raw markdown to text clients on /docs and repo pages
Clients that don't advertise text/html in Accept (curl, AI agents,
scripts) got an empty SPA shell from /docs, /docs/:slug, and
/repos/:owner/:name — the advertised agent entry points were blank
without JavaScript. Negotiate on the Accept header: text clients now
get the section's raw markdown (or a generated index for the docs hub)
and a markdown repo summary with the clone URL, owner DID, recent
signed push certificates, and raw node-API links.

Social and search crawlers send wildcard Accept headers too, so a UA
allowlist keeps serving them the meta-tagged HTML shell — link preview
cards are unaffected. All negotiated responses carry
Vary: Accept, User-Agent.
2026-08-17 16:11:52 +08:00
Adarsh Gourab Mahalik c4603e4b0c Migrate to HeroUI v3 + theme border-radius + realtime polling (#10)
* feat(theme): HeroUI v3 theme, Tailwind v4 config, file icons

Introduce the HeroUI v3 theme layer that the rest of the migration builds on:
- package: @heroui/react, @heroui/styles, lucide-react, csstype; add Tailwind v4
  postcss plugin (postcss.config.mjs), configure vite plugin / font handling;
- src/index.css: lean on @heroui/styles color-mix ranges for accent/base
  variables; drop prior redundant overrides
- src/lib/fileIcons.tsx: colorized per-extension/per-name icon map with
  dynamic Lucide icon + color tuple
- src/hooks/useTheme.ts: theme state (accent/base/font/radius), Google Fonts
  CDN link injection, localStorage persistence (incl. migration of old keys)

* refactor(realtime): remove manual refresh, poll every hook

- delete useRefreshKey (no more manual refresh key plumbing)
- useAutoRefresh polling now covers every data hook:
  - events/tasks: 30s (high-churn feeds), visibility-aware
  - repos/agents/peers/network/home: 60s
- useNodesStatus re-validates every 60s instead of one-shot/session cache
- useAgents gains a 60s visibility-aware tick (re-uses session cache
  on first load, bypasses it on each tick)
- hooks no longer accept refreshKey; render-phase reset keys tick instead

* feat(ui): HeroUI v3 component migration, Drawer fix, theme radius

Replace native HTML controls with HeroUI equivalents across components:
- Button variant="tertiary" for all clickables (repos, events, agents,
  peers, tasks, network, command palette)
- <select> -> HeroUI Select+Menu (RepoToolbar, RepoPagination)
- <a> -> HeroUI Link (PeerList, FileViewer, DocsPage) with target/rel

Fixes & polish:
- TopBar mobile Drawer: style DrawerTrigger/DrawerCloseTrigger directly
  (no nested <button>), wire close to onPress; simple SVG hamburger/close
  icons replacing lucide Menu/X
- Command Palette: custom fixed overlay (cmdk dropped), transparent
  ModalBackdrop (centered w/o dimming), body-scroll lock
- FileList: colorized per-type file icons via fileIcons.ts utility

Theme radius:
- drop hardcoded rounded-[2px]/rounded-lg/rounded-sm/rounded-md across
  components in favour of rounded-[--radius] (theme-driven)
- remove obsolete components/ui/{badge,card,button,input-group,
  separator,table,tabs,theme-toggle}.tsx (superseded by HeroUI)

* refactor(pages): drop manual refresh UI, consume realtime hooks

All list pages (repos, agents, peers, events, tasks, network, home):
- remove useRefreshKey usage and refresh/retry pills
- RepoHero no longer takes refreshing/onRefresh props (button removed)
- RepoHero accepts only display props (totalCount, page windows, title,
  description, cells)
- RepoHero no longer renders a refresh pill; the page title block is clean

The data layer (commit c71dd9c) now polls each hook on a 30s/60s tick,
so the removed Retry buttons are unnecessary — transient errors self-heal
on the next tick.
2026-08-17 15:31:53 +08:00
Kevin Codex 4df4bbe652 docs: cert-id discovery step + fresh-machine doctor expectations
From the 0.7.0 blind acceptance retest (which otherwise passed with
zero corrections): the docs told agents to verify a cert by id without
ever saying where the id comes from (gl cert list, now stated, with a
gl >= 0.7.0 note on --verify), and 'fix anything doctor flags' read as
a detour on fresh machines where identity/registration legitimately
fail until gl init runs.
2026-07-23 09:49:59 +08:00
Kevin Codex 2e4dcdedd7 docs: update to gl 0.7.0 — drop obsolete workarounds, document real cert verification
0.7.0 shipped the acceptance-test CLI fixes, so the docs stop working
around them: the gl status origin-only caveat is gone (status now
detects the gitlawb remote), the branch -M step is dropped from the
fast paths (gl init initializes main), and 'verify with gl cert' is
finally literal — gl cert show --verify checks the Ed25519 signature
and anchors the issuer (--expect-node for cross-node). Also fixes the
skill's phantom 'cert verify <cert-file>' signature that never existed,
and adds the commit step quickstart was missing.
2026-07-23 09:37:12 +08:00
Kevin Codex 86dfc5fd20 docs: use the idempotent-safe unalias form in the shadowing fix
Matches the CLI's printed advice: 'unalias gl 2>/dev/null' doesn't
error when gl isn't currently aliased (review feedback on
Gitlawb/node#236).
2026-07-22 21:44:12 +08:00
Kevin Codex 0ec2ce1ce6 docs: document the oh-my-zsh gl alias shadowing in every failure table
oh-my-zsh's git plugin ships gl='git pull', so bare gl prints git's
'fatal: not a git repository' in interactive shells — now listed with
the one-line unalias fix in agents.md, quickstart, and skill.md
(installer + brew caveats + gl doctor gain matching warnings in
Gitlawb/node#<CLI fixes PR>).
2026-07-22 20:52:04 +08:00
Kevin Codex dc7925c1df docs: add brew trust step to all Homebrew install snippets
Homebrew 6+ refuses to load formulae from untrusted third-party taps
(Error: Refusing to load formula gitlawb/tap/gl from untrusted tap);
interactive users get stuck and non-interactive agents hard-fail.
brew trust gitlawb/tap after tapping is now part of every install
snippet (agents.md, quickstart.md, skill.md, tap README).
2026-07-22 20:15:28 +08:00
Kevin Codex d5a09b9cad docs: fix the fast path and honesty gaps found by the agent acceptance test
A fresh agent given ONLY these docs against a live node reached a
verified signed push — but hit exactly the gaps fixed here:

- Fast path was broken as written: gl init does not commit, so the
  immediate 'git push gitlawb main' fails with 'src refspec main does
  not match any' (and fresh repos may default to master). The commit +
  branch -M steps are now in the path, in skill.md, and in both
  edge-case tables.
- gl status doesn't detect the 'gitlawb'-named remote gl init creates
  (origin-only) — documented so the misleading 'not in a gitlawb repo'
  doesn't read as failure.
- 'verify with gl cert' oversold: show prints the payload and offline
  verification instructions, it doesn't return a verdict — reworded to
  inspect, with the actual subcommand syntax.
- Remote owner segment documented as the bare z6Mk… key (did🔑
  prefix optional), matching what gl init actually writes.
2026-07-22 16:43:28 +08:00
Kevin Codex 764fd90131 feat: docs share pages — wire og-docs.png into /docs crawler metadata
/docs and /docs/:slug now rewrite to api/docs-page.ts, which swaps the
marker-delimited social-meta block in the built shell for per-section
title/description and the og-docs.png card (same mechanism as
repo-page; helpers deliberately duplicated so the prod-verified
repo-page stays untouched). Raw /docs/*.md is unaffected — the
filesystem is served before rewrites. DocsPage mirrors the titles
client-side during SPA navigation. Unknown slugs get hub metadata
without echoing the input.
2026-07-22 15:46:09 +08:00
Kevin Codex 35ffdfae80 fix: ESM-proof the api function imports; inline the OG layout
Follow-up to the repoSocial move — two more tracer/runtime mismatches
kept api/og crashing:

- The package is type:module, so compiled functions run as Node ESM and
  extensionless relative specifiers never resolve. All cross-file api
  imports now carry explicit .js extensions (TS, Vite, and vitest all
  resolve .js -> .ts natively).
- The tracer maps .js specifiers to .ts sources but not .tsx, so an
  imported .tsx module is silently dropped from the bundle. og-layout is
  inlined into the og.tsx entrypoint (entrypoint .tsx compiles fine);
  its exports remain for the tests.

Verified against a local vercel build: both compiled functions import
cleanly under node --input-type=module, and GET on og.js renders a real
200 image/png including the traced JetBrains Mono TTFs.
2026-07-22 14:01:39 +08:00
Kevin Codex 04e8e8be6d fix: move repoSocial into api/_lib so Vercel functions can import it
Both api/repo-page and api/og crashed in production with
ERR_MODULE_NOT_FOUND on /var/task/src/lib/repoSocial: Vercel compiles
TypeScript under api/ only, so a function importing ../src/lib/* ships
an import specifier the Node runtime can't resolve — and the
/repos/:owner/:name rewrite meant every direct repo-page load 500'd.
The module was already runtime-agnostic by design; it now lives in
api/_lib with the browser hook importing across, and tsconfig.api's
src special-case is gone.
2026-07-22 13:54:57 +08:00
Kevin Codex c682af2e9e feat: per-repo social metadata — SSR share pages + dynamic OG images
Crawler-visible metadata for /repos/:owner/:name without turning the SPA
into an SSR app: a Vercel function fetches the built index.html, swaps
the marker-delimited social-meta block for repo-specific tags, and a
sibling function renders per-repo OG cards with @vercel/og (JetBrains
Mono TTFs; Satori can't consume WOFF2). Private repos and upstream
errors always get the generic site metadata plus noindex — repository
data is never interpolated on those paths. The client hook keeps
document meta in sync during SPA navigation, and the /api proxy narrows
to /api/v1 so local functions are reachable. Cache tiers: immutable
versioned OG URLs, 5m/1h SWR for pages, no-store on errors.
2026-07-22 12:48:19 +08:00
Kevin Codex 2ec020a3dc feat: OG image for the docs hub
1200x630 sibling of og.png — same grid, ticker bar, JetBrains Mono
wordmark with the warm dot, and terminal bottom row. Source SVG kept
in reference/ (rasterized with resvg + bundled JetBrains Mono TTFs).
2026-07-22 12:31:11 +08:00
Kevin Codex 36f21d6e2b feat: docs hub — agent, human, and protocol docs with raw markdown + skill.md + llms.txt
Adds a /docs section to the explorer so technical content lives beside the
live network data instead of hardcoded in the website:

- /docs/{quickstart,agents,protocol,node} rendered from public/docs/*.md
  through the existing marked+Shiki+DOMPurify pipeline (new
  renderDocsMarkdown variant without repo-specific link rewriting),
  with the sidebar, TocRail scroll-spy, and markdown-gl typography.
- The same markdown files serve raw at /docs/*.md for machine readers;
  /skill.md is the installable agent skill (v2: gl 0.6.0, gl init fast
  path, iCaptcha behavior documented, full current CLI reference);
  /llms.txt indexes everything.
- Every command in the agent docs was verified against gl 0.6.0 and a
  live node round-trip (init, signed push, clone, incremental push).
- docs tab added to ExploreTabs.
2026-07-22 10:33:04 +08:00
Kevin CodexandClaude Fable 5 a23f2249a8 Add node home, peers, events, tasks, and network sections (#8)
Expands the explorer from repos+agents into a full replacement for
gitlawb.com/node, with URL paths matching the old site 1:1 (minus the
/node prefix) so a single redirect covers the migration:

- / — node overview dashboard: identity, stat tiles, live activity
  feed, agent-task summary, p2p status, quick clone
- /peers — searchable peer directory with live per-peer ping checks
- /events — node-level ref-update feed with local/gossip filter and
  visibility-aware 30s auto-refresh
- /tasks + /tasks/:id — agent task queue with server-side status
  filter, payload-derived titles, and pretty-printed payload/result
- /network — fan-out to the 4 federation nodes with per-node cards
  and a replication-coverage table over their event feeds

Peer nodes serve no CORS headers, so node2/node3/manila get proxy
prefixes in vercel.json and the vite dev proxy. Section nav moves
from per-page tabs into the global layout.

Co-authored-by: Claude Fable 5 <[email protected]>
2026-07-05 22:32:29 +08:00
Kevin CodexandClaude Fable 5 c48bede2fa Add Open Graph and Twitter card metadata with branded share image (#7)
* Add Open Graph and Twitter card metadata with branded share image

Sharing the explorer link previously produced a bare, imageless card.
index.html now carries og:/twitter: metadata with absolute URLs (link
scrapers don't resolve relative paths or run JS), and public/og.png is
a 1200x630 card in the app's own terminal aesthetic: blueprint grid,
gitlawb mark + "explorer." wordmark, and a real repo row with activity
sparkline. 41 KB, rendered from a JetBrains Mono HTML template.

Co-Authored-By: Claude Fable 5 <[email protected]>

* Feature openclaude in the OG card repo row

Real repo data from the node: z6MkqDnb/openclaude, its actual branch
and update recency, described by its README one-liner.

Co-Authored-By: Claude Fable 5 <[email protected]>

---------

Co-authored-by: Claude Fable 5 <[email protected]>
2026-07-03 09:56:00 +08:00
Kevin CodexandClaude Fable 5 0253e5c31a Add Vercel rewrites for API proxy and SPA fallback (#6)
The Vite dev proxy (/api, /node-info → node.gitlawb.com) only exists in
the dev server, so the deployed static build 404'd every API call.
vercel.json now mirrors the proxy in production and adds an SPA fallback
so client-side routes survive refresh/deep links. Also ignores the
.vercel project-link directory.

Co-authored-by: Claude Fable 5 <[email protected]>
2026-07-03 09:47:05 +08:00
Kevin CodexandClaude Fable 5 253669bb58 Rebrand to gitlawb logo and polish the repo detail page (#5)
Branding: the official gitlawb mark (extracted as a 4KB currentColor SVG
from the platform brand asset) replaces the text-only header branding —
now [logo] explorer — and the purple-bolt favicon, which becomes the
mark on a black tile. Title and footer read "gitlawb explorer".

Detail page:
- Owner DID middle-truncates via new truncateDid() (did:key:z6Mkn…xXxYk),
  full DID on hover and in the copy button; footer node DID reuses it.
- Right stats column widens 380px → 440px with rebalanced cells so the
  commit hash and created date sit on one line.
- README render and file markdown preview drop the 72ch cap; the code
  viewer drops its 1100px cap — both now fill the content width.

Co-authored-by: Claude Fable 5 <[email protected]>
2026-07-03 09:28:06 +08:00
Kevin CodexandClaude Fable 5 67aa8f6072 Redesign repo list rows with activity sparklines (#4)
Rows go from a dense single line to a taller two-line layout (name +
pills, full-width description) with a per-repo commit-activity sparkline
in a new column, GitHub-style.

- Sparkline: dependency-free SVG micro bar chart of weekly commit counts
  (12 weeks); past weeks in a de-emphasis tone, current week in the warm
  accent. Values carried by an aria-label; counts at the node's
  30-commit API cap are reported as a floor ("30+").
- useRepoActivity: IntersectionObserver-gated lazy load per row, session
  cache, and a 2-slot concurrency pool since the node limits concurrent
  connections. Hover-prefetched repos (repoCache) resolve without a
  fetch via the new RepoCommit.dateRaw field.
- RepoList header and RepoRowSkeleton updated to the new grid.

Co-authored-by: Claude Fable 5 <[email protected]>
2026-07-03 08:31:53 +08:00
Kevin CodexandClaude Fable 5 d77cd9a846 Make repository open-source ready (#3)
- Add Vitest unit test suite (60 tests) covering the pure-logic modules
  in src/lib/: fuzzy matcher, line-range permalinks, language detection,
  API helpers/classification, and TOC extraction
- Rewrite README with features, quick start, API docs, project structure,
  and testing guide
- Add MIT LICENSE, CONTRIBUTING.md, and a PR template
- Add GitHub Actions CI (lint, test, build)
- Rename package to gitlawb-node-explorer with proper metadata

Co-authored-by: Claude Fable 5 <[email protected]>
2026-07-03 07:36:52 +08:00
Kevin Codex 668440c678 Merge pull request #2 from Gitlawb/redesign/terminal-explorer
Redesign/terminal explorer
2026-07-03 06:12:07 +08:00
Kevin Codex a67f168f28 implemented redesigned UI/UX 2026-07-03 06:11:03 +08:00
Kevin CodexandClaude Fable 5 dba2a2683b Redesign explorer with terminal aesthetic and real-scale data
Visual overhaul to match the gitlawb terminal mockups in reference/:
JetBrains Mono throughout, dark-first near-black theme (light kept as
secondary), grid-line hero panels, uppercase micro-labels, square pills,
and a footer carrying the node DID and version. Removes the inline-style
+ JS hover pattern and dead components (Navbar, PageHeader).

Points at production node.gitlawb.com (82k repos) and replaces the
fetch-everything data layer with server-side pagination via limit/offset
and X-Total-Count. List state is URL-driven (?page=&per=&q=&sort=&owner=).
Search/sort run client-side per page until VITE_SERVER_SEARCH enables the
node's q=/sort= params.

New: agents explore tab (capabilities, trust tiers, owner->repos link),
sort + fork filters, debounced labeled search, skeleton loading states,
real issues/pulls/events/certs lists in repo detail with error + retry,
working browse-code/pull-requests buttons, node stats in the top bar.

Fixes: doubled did:key prefix in detail header, duplicate star count in
StatsPanel, null-description crash in search, keyboard-inaccessible rows,
hover-only clone button, header/content width mismatch.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-03 06:10:56 +08:00
Kevin Codex eb51e69cd5 Merge pull request #1 from khushie09/main
Redesign repository explorer UI and improve responsiveness
2026-07-02 09:32:58 +08:00
Khushi Chetule 9e2b4f4619 Integrate real API and fix repository detail overflow 2026-06-16 17:51:38 +05:30
Khushi Chetule 5fa738e00e Redesign repository explorer UI and improve responsiveness 2026-06-11 19:21:02 +05:30
Kevin Codex dd8cf9ce15 Initial commit 2026-06-04 21:25:08 +08:00