Architecture
How the mod, the bridge process and Supabase fit together, and why.
Overview
Section titled “Overview”
flowchart LR
subgraph cc["Claude Code process"]
mod["squad-chat mod<br/>(hooks/*.mjs)"]
end
subgraph bridge["bridge child process (Node ≥ 22)"]
b["dist/bridge.mjs<br/>supabase-js"]
end
subgraph sb["Supabase"]
auth["Auth<br/>email codes"]
db[("Postgres<br/>RLS")]
rt["Realtime<br/>private room channels"]
end
mod -- "$.process.spawn" --> b
b -- "NDJSON events (stdout)" --> mod
mod -- "HTTP over a private Unix socket" --> b
b -- "HTTPS" --> auth
b -- "HTTPS (PostgREST, RPC)" --> db
b <-- "WebSocket" --> rt
- The mod (
plugins/squad-chat/hooks/) runs inside Claude Code. It draws the pane, the band above the prompt and the status line, registers the commands, and keeps chat out of the conversation. - The bridge (
plugins/squad-chat/bridge/) is a Node child the mod starts. It holds the Supabase session and the Realtime connection, because the mod runtime has no WebSocket and can’t load npm packages. - Supabase stores profiles, rooms, members and messages behind row-level security, and pushes presence and new messages over private Realtime channels.
Why a bridge process
Section titled “Why a bridge process”These findings come from the Phase 0 spike on Claude Code 2.1.291:
| Question | Answer |
|---|---|
| Can the pane take keyboard input? | Yes. An Input element with onSubmit, in a pane opened with focus. |
| Can background events redraw the pane? | Yes. The bridge’s NDJSON on stdout → $.ui.invalidate("ui.render"). |
| Realtime without WebSocket? | Mods have no WebSocket, so a Node child ($.process.spawn) holds the connection. The mod controls it over HTTP on a private Unix socket ($.http.fetch with socketPath, directory 0700, per-run token). |
| Is there a reliable “session ended” event? | session.end exists but has a short time budget, so it is best effort. The bridge also exits by itself within about 5 s when its parent dies (it watches ppid), and presence relies on the dropped connection. |
| Does chat leak into Claude’s context? | Text typed in the pane never enters the transcript. A slash command is recorded as a user row with its arguments, so the mod rewrites the arguments of /say, /room and /chat-login in session.append, failing closed. Verified headless: the model can’t see them. |
One rule of our own comes first: what arrives from a room never enters the model’s context. The mod has no prompt.submit, prompt.compose or tool.call hook, and nothing it receives from the bridge is appended to the session. Friends’ messages are drawn, never read by Claude, so one that reads like an instruction stays a chat message. If a feature ever needs Claude to read a room (say, “summarize what I missed”), the person has to ask for it each time, and the text goes in as a clearly marked block of untrusted data, never as part of the prompt. room messages never reach the model in the mod tests guards this.
Two rules of the mod runtime shape the code:
$is never followed across an import, so everything that touches the engine lives inhooks/squad-chat.mjs.state.mjs,commands.mjs,views.mjsandtheme.mjsare plain logic. The hooks module hands them acall(path, body)function, asay(text)function and the surface’s element table.Texttakes no flex props. A tree with one is refused whole and the engine draws its own, so anything that must not shrink is wrapped in aBox.
Backend
Section titled “Backend”supabase/migrations/ holds the schema:
- Tables:
profiles(display name from the email’s local part, made unique),rooms(bcrypt passcode hash, never readable by clients),room_members(read markers),messages(eachtext, or a sharedcodeordiffsnippet with an optional language tag) andpresence_heartbeats. - Access: RLS on every table, column-level grants to
authenticatedonly, nothing foranon. You see a room, its members and its messages only while you are a member. - Accounts: a profile is created for every new user, named after the email’s local part, or for an anonymous account after the name it picked (made unique with
-2,-3, …). - Joining: only through
join_room(slug, passcode), which creates the room if it doesn’t exist and locks a caller out for 15 minutes after 5 wrong passcodes. Privileged helpers live in an unexposedprivateschema. - Deleting: only the room’s creator can delete it, and its members and messages go with it.
- Limits: a chat message is at most 500 characters, a snippet 8000 characters and 200 lines. A trigger caps each user at 10 messages per 10 seconds, and at 3 snippets a minute.
pg_crondeletes messages older than 30 days. - Renaming: people may change only their own display name, and names stay unique.
- Realtime: private
room:<uuid>channels. Policies onrealtime.messageslet only members receive a room’s channel, track presence on it, or broadcast on it (typing indicators). New messages arrive throughpostgres_changes, filtered by the table’s own RLS.
supabase/tests/rls.test.sql checks all of this with pgTAP: non-members see nothing, nobody can post as someone else or backdate a message, wrong passcodes fail and lock out, the flood limit holds, only creators delete rooms.
There is no central server: each group runs its own Supabase project, and the plugin’s options (supabase_url, supabase_key, from userConfig) point at it. The mod passes them to the bridge as SQUAD_SUPABASE_URL and SQUAD_SUPABASE_KEY. Unset options leave those environment variables alone, which is how local development points at supabase start. With neither set, the bridge exits with an unconfigured error and the pane explains how to connect.
Two ways to sign in, both through Supabase Auth:
- A name: an anonymous account (
signInAnonymously), its display name passed as user metadata and picked up by thehandle_new_usertrigger. Needs anonymous sign-ins switched on, and no email service. - An email code:
signInWithOtp, thenverifyOtpwith the 6-10 digit code. Supabase’s built-in email only reaches the project’s team, and new free projects can only change their templates once custom SMTP is set up, so this needs an SMTP provider and templates showing{{ .Token }}.
Bridge
Section titled “Bridge”plugins/squad-chat/bridge/src/:
bridge.mjs: the process. Control API over the Unix socket (/login/name,/login/start,/login/verify,/logout,/name,/room,/room/select,/room/leave,/room/delete,/send,/typing,/status,/read,/who,/state,/sessions/beat,/fnroom/list,/fnroom/visible,/fnroom/refresh,/fnroom/action,/ping,/shutdown), NDJSON events on stdout (ready,auth,rooms,message,presence,typing,name,friends,status,sessions,fnrooms,fnroom,error), and the parent watch. Without a server configured it still starts, for the heartbeats alone: it reportsunconfigured, itsreadysayschat: false, and the chat routes answer 503.chat.mjs: everything Supabase. Sign-in with the emailed code, rooms, one private channel per room, catch-up, unread counts, heartbeats and the friends list.sessions.mjs: the heartbeat board for the Agents room (see Built-in rooms).rooms/: the function rooms (see Function rooms):manifest.mjschecks a room’s manifest,registry.mjsloads the rooms and runs their providers,net.mjsholds what a provider may fetch, andproviders/has one file per provider.file-storage.mjs: the session file,~/.config/squad-chat/session.json, written0600through a temp file and a rename.
Things that took a while to get right:
- Catch-up without gaps. Channels subscribe with
postgres_changes_options: { wait: true }, soSUBSCRIBEDmeans delivery is live. Only then does the bridge fetch what it missed (id > lastSeenId, with a small overlap, de-duplicated by id). - Reconnecting. realtime-js retries once after the connection drops. If that attempt fails while the network is still down, the socket stays “connecting” forever. A watchdog forces
disconnect()thenconnect()after 10 s offline, with backoff. - Unread counts. The bridge owns them: the database’s count when a room is first seen, then each live or caught-up message from someone else past the read marker. Recounting later would double what catch-up adds.
- Presence. Online means present in any room’s channel, or a heartbeat newer than that person’s last presence leave. Without that rule, a recent heartbeat kept someone “online” for minutes after they quit.
- Typing. Keystrokes in the pane reach the bridge at most every 2 seconds, and the bridge broadcasts at most that often per room. Receivers show someone as typing until their message arrives or 5 seconds pass. The broadcast carries only a user id, and the name comes from the receiver’s own records, so nobody can type under a made-up name.
- Do not disturb. The mod decides when it holds: switched on, or
autoand a Claude turn has run past 30 seconds (turn.start, thenturn.completeof the main turn, not a subagent’s). It tells the bridge through/status, and the bridge addsstatus: "busy"to what it tracks on every room channel, so roommates see it through presence at once. A restarted bridge starts out available, so the mod sends it again after sign-in. - Snippets.
/chat-sharereads the session on the mod’s side ($.ui.selection(),$.session.messages(), orgit diff HEADthrough$.process.runin the session’s folder) and only reads it: nothing is added to the conversation. It shows a preview first and checks for likely secrets. The bridge sends it withkindcodeordiff, keeping indentation. A server that hasn’t run the snippets migration has nokindorlangcolumn: the bridge notices the missing column, reads messages without them, and refuses snippets with a hint to runsupabase db push, so chat keeps working. The slash command is/chat-sharebecause Claude Code has a/shareof its own. Commands are registered one at a time, so a name Claude Code refuses costs only that command. - Renaming. A new name goes to the database, then out through presence, so roommates see it at once. The bridge tells the mod when a known name changes, and the pane relabels messages already on screen.
dist/bridge.mjs is one file bundled by esbuild and committed, so installing the plugin needs no npm install.
Built-in rooms
Section titled “Built-in rooms”The Usage, Git, Agents, Focus, Clipboard and Pet tabs are drawn by the mod from what it can read on this computer. They add no tables and need no sign-in.
- Usage.
session.measurepushes context, rate limits and cost whenever they move, and$.session.usage()reads them when the room opens (withbreakdown: "summary"for the context breakdown, estimated locally at no cost).turn.completeresolves with each turn’s token usage, the main loop’s and every subagent’s, which gives the cache hit ratio. Atool.callhook times each call aroundnext(e)and passes the result through unchanged. It never denies a call, and its.catchreplays the call’s own result.agent.spawnand$.agent.list()give the subagents and their status. All of it lives inmetrics.mjsas plain data. - Git.
github.mjsrunsgit status --porcelain=v2 --branchand a handful ofgh … --jsoncalls in parallel through$.process.runin the session’s folder, as the person is already signed in togh. It polls every minute while the room is on show and every five minutes otherwise, and once more a few seconds after agit pushorgh pr creategoes through the Bash tool. A failed refresh keeps the last snapshot, marked stale. Comparing two snapshots gives the toasts: checks failing, a review asked of you, your PR approved, merged or in conflict. - Agents. Each session’s mod builds a heartbeat (
sessions.mjs): folder name, branch, model, what it’s doing, its subagent tree and its last dozen tool calls, each summarized in a few words with secret-looking text masked. It posts it to its bridge at most once a second while things change, and every ten seconds otherwise. The bridge writes it to/tmp/squad-chat-<uid>/sessions/<session id>.json(folder0700, file0600, through a temp file and a rename), watches the folder, and sends the mod asessionsevent with every live heartbeat. A file is dropped when its bridge’s pid is gone or it hasn’t been written for 30 seconds, and a bridge deletes its own file when it shuts down. The folder sits beside the sockets, not in the config folder, so sessions under differentSQUAD_CONFIG_DIRs still see each other. - Focus.
focus.mjsis the Pomodoro timer as plain data: a phase (idle,focus,break), when it ends or what was left at a pause, and today’s count. The mod’s one-second loop checks it on$.clock.now(), so a test can run a whole round on the mocked clock. A focus round that runs out playssounds/focus-done.wavthrough$.audio.play(afplayon a Mac), toasts even during do not disturb (a timer you set should ring), and starts the break. The lengths, the day’s count and a year of rounds per day (for the streak card) go to$.storefor every session, and a running timer underfocus:<session id>, so a reload carries on and two sessions never ring for one timer. - Clipboard. While the room has a tab, the loop runs
pbpaste(orwl-paste, orxclip, the first that works) through$.process.runevery 2 seconds.clipboard.mjskeeps the last 30 texts in memory, newest first, moving one copied again back to the top. Text thatfindSecretflags is drawn masked, and the room has no snapshot, so/chat-share clipboardrefuses. It’s left out of the default tabs, since it reads what you copy. - Pet. On the desktop
clawdsvg.mjsdraws Clawd as shapes on a 32-unit grid (the pixel stickers’ shape and colors, rounded), one drawing per mood, each moving by CSS animations inside it (a bob, a wave, notes floating up), so it moves with no redraws and stays sharp at any size. The terminal has no Svg, so it draws the pixel stickers inpetframes.mjs(from the clawd-stickers pack, 32×32 frames as run-length rows) in half blocks,▀in the upper pixel’s color on the lower one’s, and redraws as fast as the frames go while the room is on show.petMoodpicks a mood from the session: the turn and its tools, the Focus timer, new messages and failing checks.petTickadds up the time Claude works, the bond (xp from focus rounds and time together) and the log of what Clawd noticed, which says who wrote in which room but never what. They go to$.storewith the mood picked. By the desktop’s input box Clawd walks a track drawn as one SVG with no width of its own (100%), so it never scales to its slot. Left without a width the desktop draws such an SVG 300 px wide, so theSvgis given more than the pane could hold (from its columns) and the slot holds it to the room there is: SMIL moves it from 0% to 100% across while a translate takes off its own width, so it turns at both ends exactly at any width, and nothing redraws while it walks. Stroll and Stop & go take the room after the field’s buttons. - Drawing.
sysviews.mjsbuilds each room from the same rounded card as the chat, withwidgets.mjsfor eighth-block meters, sparklines, stat tiles and rows whose fixed parts never shrink.stackCardsplaces cards by priority in the height the dock has and names the ones that didn’t fit. One loop in the mod ticks every second: a redraw while something runs (spinners and timers), the subagents’ status, the heartbeat and the Git poll.
Function rooms
Section titled “Function rooms”A function room is a room drawn from data, not from code of its own: a manifest, rooms/<id>/room.json, names its providers, the hosts they may reach and a layout. Snippets is the first. The manifest is data: nothing in it runs, so a room someone else wrote can’t do more than the providers squad-chat ships allow.
{ "schema": 1, "id": "snippet", "version": "1.0.0", "name": "Snippets", "icon": "⌘", "color": "amber", "permissions": { "hosts": [] }, "providers": [{ "id": "list", "type": "local-list", "params": { "max": 200 } }], "layout": { "cards": [{ "title": "SNIPPETS", "meta": "{list.count} saved", "body": { "type": "list", "items": "list.items", "title": "name", "tag": "lang", "copy": "body", "share": "body" } }], "band": "{list.count} snippets" }}- Where rooms come from. The bridge reads the rooms squad-chat ships (
plugins/squad-chat/rooms/, passed asSQUAD_ROOMS_DIR), then any in~/.config/squad-chat/rooms/, which can’t take a shipped room’s id.manifest.mjschecks each one: its id, name, icon and color, that every provider exists, the hosts, and that the layout uses only known widgets with plain paths. One that doesn’t check out is skipped, and the reasons go to the debug log. - Providers. Each is a module in
bridge/src/rooms/providers/with atype, thehostsit may ever reach,fetch(params, ctx)for its data and namedactions(Snippets hasadd,renameanddelete).ctx.fetchreaches only the hosts both the provider and the manifest name, within 10 seconds and 1 MB, and refuses redirects.ctx.dataDiris the room’s own folder under~/.config/squad-chat/room-data/. Every string in a provider’s answer loses terminal escapes and control characters before it leaves the bridge. An action is looked up in a table of the ones the provider declared, never by a property name from the request. - One list, many sessions. Every session’s bridge can change the Snippets list and a room’s settings, so each change holds a lock (
lock.mjs: a folder made beside the file, which is atomic, with anownerfile holding the holder’s pid and a token). A lock is broken only when its holder’s pid no longer runs, never for being slow, by renaming it aside so that only one waiter can, and the winner puts it back if what it moved turns out to be live. Releasing removes a lock only while it’s still the holder’s. A list it can’t read is reported and left alone: only a missing file counts as empty. A provider asked to run while it’s running runs once more afterwards, so an action’s change is never overwritten by an older answer. Settings are read again only when the file’s inode or mtime changes. - Settings. A manifest may declare up to 8 settings (
list,enum,string,boolorint, each with a default), and a provider’s params name one as"$settings.<key>"./chat set <room> <key> <value>(or/setin the room) sends what was typed to/fnroom/settings, where the bridge reads it as the setting’s kind (a list takesa, b,+cor-a), checks it, keeps the changed ones in the room’ssettings.json(0600) and runs the room again. A list of URLs must stay on the room’s hosts. The bridge reports each room’s values as anfnsettingsevent. - Rooms that go online (Weather and Tech News so far) are off until
/chat rooms +name, which names the hosts they reach. Weather’sopen-meteoprovider looks a city up once, then fetches its forecast and air quality.hnreads Hacker News’s Firebase API.rssreads RSS and Atom without an XML library. A feed reader goes wherever its room points it, so it declares its hosts as"*"and the manifest’s list alone decides. - Quotes. The Stocks room’s
quotesprovider asks TWSE’s MIS service for Taiwan codes (bothtse_andotc_, since a bare code could be either, and the answer says which) and Yahoo’s chart endpoint for everything else and for every quote’s intraday line. A market counts as open only when its data is from today and the clock is inside its session, as TWSE answers on holidays with the last trading day’s prices. While every market is closed it reuses its last answer for 15 minutes. If TWSE fails, Yahoo’s.TWquote stands in. A table column’scolorFromnames a field holding a palette color, so each row’s change can be red or green. - Any JSON API.
http-jsonreads one https URL on the room’s hosts ("*", likerss), its{holes}filled fromvars, which are usually settings. It turns an object of objects (CoinGecko’s prices, Frankfurter’s rates) into rows, keeps them in a setting’s order, and formats fields by path:number,compact,percent,signed,signed-percent,date,age. Each field gives a display string and its number, and a signed one an arrow and a palette color forcolorFrom. A change that rounds to nothing shows flat.$settings.<key>is now filled at any depth of a provider’s params, and the manifest check follows it there. - A manifest’s alerts.
alertswatch each row’s field (rowsandfield) or onevalue, pastabove,beloworbeyonda number or a setting, where a setting of 0 is off. The registry checks them on each provider’s fresh data and adds them to itsalerts, with ids prefixedm:, so the mod toasts them as it does a provider’s own. - Controls. A layout’s
buttonswidget, a list row’sact(Lo-fi’s ▶) and the layout’skeys(single keys typed in the room) each name one of the provider’s actions, checked against it when the manifest loads. The mod sends them to/fnroom/action. - A process that keeps running. Lo-fi’s
playerprovider starts one mpv per bridge, only on play, and it belongs to the room that last pressed play (another room using the provider sees nothing playing and can’t drive it). A manifest’s stations must be https and on the room’s own hosts, checked with the manifest and again before playing, since mpv fetches them itself. It starts throughctx.spawn:--idle --no-video, an IPC socket in the bridge’s short runtime folder, under ashwatchdog that stops mpv within a second of the bridge going away (akill -9too), and in its own process group. It talks to mpv over the socket (loadfile,cycle pause,add volume,get_property), looks up a YouTube channel’s live streams with yt-dlp when one is played, and checks whether Pixel Play’s mpv is playing too. A provider’sclose()runs when the bridge shuts down, and stops the group. - Reading this computer. Monitor’s
sysinfoprovider runs commands throughctx.run(argv): no shell, 3 seconds and 4 MB at most, and only commands named in the provider’s own code, never in a manifest.ctx.read(file),ctx.cpus()andctx.loadavg()cover/proc,/sysand the CPU counters. Rates and charts need the sample before, so it keeps each room’s last sample and 60 points of history in memory. On a Mac, temperatures and power by part come from macmon (macmon pipe -s 1 -i 250, about 0.4 s) when it’s installed, since macOS keeps them from anything without root. - Alerts. A provider’s data may carry
alerts: [{ id, text }]. The mod toasts each id once, again only after it has gone and come back, and not while do not disturb holds. - When they run. The mod tells the bridge which function rooms have tabs and which is on show (
/fnroom/visible, only when that changes). A provider runs when its room gets a tab, after each of its actions, on/fnroom/refresh(rin the room), and on itsinterval:visiblewhile its room is on show,backgroundotherwise. A failed run keeps the last data, marked stale, with the error. - Drawing.
fnviews.mjsfills the layout in from the providers’ data:list,table,tiles,meterandtextwidgets, one to a card or a column of up to six, and a card withwhenshows only while that path has data (Monitor’s GPU and battery cards), bound with paths such aslist.itemsand templates such as{list.count} saved. One layout gives the docked cards (a long list gives up rows, “+ 3 more”, before a card is left out), the inline pane, the band and the plain-text snapshot/chat-share <room>posts. - Tabs. Function rooms sit after the built-in ones.
/chat roomstakes both, with+nameand-name. A room that’s new in a version gets its tab once, even for people who chose their tabs before (roomsOfferedin$.store). Taken away, it stays away.
The room store
Section titled “The room store”Rooms anyone writes live in the repository’s rooms/, one folder each, with an index.json that rooms/build-index.mjs builds: each room’s id, name, version, minSquadChat, hosts, providers and sha256. CI runs it with --check, so a room that doesn’t check out, or an index that’s out of date, fails the pull request. The docs site makes its Room store page from the same index.
The bridge’s store.mjs reads the index from raw.githubusercontent.com (SQUAD_ROOM_STORE points elsewhere, for tests) through the same limited fetch as the providers, and caches it for 10 minutes. /store/preview downloads a room’s room.json, checks its sha256 against the index, runs the manifest check, and compares its id, version and minSquadChat with the index and with this squad-chat. It holds that copy, and says which hosts it reaches and which are new since the installed version. /store/install writes the held copy, by its hash, to ~/.config/squad-chat/rooms/<id>/room.json (0600), and the registry reads its folders again. /store/uninstall removes the room and its room-data, and refuses a shipped room. The registry also skips any room whose minSquadChat is newer than this squad-chat, read from plugin.json.
The mod’s /chat install and /chat uninstall take two runs a minute apart. /chat update goes straight through unless the new version reaches new hosts. An installed room gets a tab, an uninstalled one loses it.
The sha256 catches a download that went wrong, not a store that lies, since both come from one place. What keeps a store room safe is that a manifest can’t run code, can’t reach a host it doesn’t name, and is shown, hosts and all, before it’s installed.
Function rooms: Phase 0
Section titled “Function rooms: Phase 0”Function rooms are rooms you install: a JSON manifest drawn with the built-in rooms’ widgets, fed by data providers that run in the bridge. A manifest never runs code. These findings come from the Phase 0 spike on Claude Code 2.1.293, macOS 27 on an M2 Max, Node 22:
| Question | Answer |
|---|---|
| Will more tabs fit? | They already wrap (flexWrap: "wrap"). With nine rooms and three chat rooms they take three rows at the dock’s usual 40-56 columns, two at 80. Drawing every room but the one on show as its icon alone brings that to two rows at 40 columns. Function rooms get icon tabs, with a Select for the rest if even that runs long. |
| Can the pane edit a multi-line snippet? | No. Input is one line, and Box clips (overflow: "hidden") without scrolling. Snippets are added from the selection or Claude’s last code block, as /chat-share reads them, and only named or renamed in the pane. |
| What can Monitor read without sudo? | CPU from os.cpus() deltas. Memory from vm_stat, sysctl vm.swapusage and kern.memorystatus_level (pressure). Network from netstat -ib deltas. Disk from df -k and iostat. Battery from pmset -g batt. The power coming in (SystemPowerIn, in mW) from ioreg -rn AppleSmartBattery. GPU use (Device Utilization %) from ioreg -rc IOAccelerator. Each takes a few milliseconds. |
| And temperatures and power by part? | Not without help. powermetrics needs root, and ioreg exposes no CPU, GPU or battery temperature on Apple Silicon. macmon 0.9 (MIT, brew install macmon) reads them without sudo: macmon pipe -i <ms> prints one JSON line per sample with CPU and GPU temperature, power for the CPU, GPU, ANE and whole system, each core’s load and frequency, fans, RAM and swap. Left running at a 2-second interval it used no measurable CPU and 18 MB. Monitor runs it as a child through the same watchdog as mpv when it’s installed, and leaves those cards out when it isn’t. |
| Can the bridge play music? | Yes, the way Pixel Play does: mpv --idle --no-video --input-ipc-server=<socket>, driven with JSON commands over the socket. A direct stream starts in about a second and names the song through icy-title. A YouTube live stream starts in about two seconds through yt-dlp, its title as media-title. |
| Does mpv stop when the bridge dies? | Not by itself: after a kill -9 of its parent it played on. Started through a small sh watchdog that checks the parent every second and stops mpv once it’s gone, it exits within a second. |
| Socket paths? | macOS caps a Unix socket’s path at 104 bytes, so mpv’s socket goes in the bridge’s short /tmp/squad-chat-<uid>/ folder, like the others. |
| Lo-fi links that last? | YouTube live streams change id: lofi girl’s best-known one now answers “not available”. The room lists a channel’s current streams (yt-dlp --flat-playlist <channel>/streams) instead of keeping ids. |
| Which stock sources work without a key? | TWSE’s MIS endpoint (getStockInfo.jsp?ex_ch=tse_2330.tw|otc_6488.tw) for Taiwan, about 130 ms, and twenty requests in a burst all answered. Yahoo’s chart endpoint (/v8/finance/chart/<symbol>) for the US and Taiwan alike, about 100 ms, also twenty in a burst. Yahoo’s /v7/finance/quote now answers 401, so it’s out. |
| When is a market open? | Hours alone don’t tell. On a holiday TWSE keeps answering with the last trading day’s prices (d is that date), so a quote counts as live only when its date is today in the exchange’s time zone. Yahoo’s currentTradingPeriod gives the session’s start and end. |
| Suite | Runs | Covers |
|---|---|---|
supabase/tests/rls.test.sql |
supabase test db |
Access control, limits, cascades (pgTAP) |
bridge-tests/ |
npm test in plugins/squad-chat/bridge |
Two users, two real bridges, local Supabase: sign-in, passcodes, presence, messages, a network drop through a cuttable proxy, restarts, unread, deleting rooms, kill -9 |
bridge-tests/sessions.test.mjs |
the same npm test, no Supabase needed |
Two bridges without a server sharing heartbeats, a dead session dropped, refusing bad heartbeats, cleaning up on shutdown |
bridge-tests/fnrooms.test.mjs |
the same npm test, no Supabase needed |
Loading rooms and refusing bad manifests, the Snippets list (add, rename, delete, a private file, kept across restarts), fetch held to a room’s hosts, time and size, and strings cleaned of terminal escapes |
plugins/squad-chat/tests/ |
claude plugin test ./plugins/squad-chat |
The mod against a fake bridge: sign-in, views, band, status line, read markers, mentions, tabs, room commands, room text never reaching the model, the built-in rooms (usage figures, tool timing passed through untouched, gh results and toasts, other sessions, snapshots), and function rooms (the Snippets room, /snippet, icon tabs, the band, stale data) |