Settings → AI Proxy gains an admin-only Update 9router button beside
Config 9router. One in-memory job (server/lib/nine-router-update.js) stops
9router (its user unit, else this account's own 9router processes), runs
npm install -g --prefix <its prefix> 9router@latest (sudo -n only when
sudoers already allows it, probed before anything stops), then relaunches
it hidden only if it was running. A failed update still relaunches the old
version. Nothing in the request names a command, path or package.
A terminal mounted on a hidden tab sits in a 0x0 box; its first fit shrank
xterm to 12x6 and the attach carried that size to tmux, so every background
pane was squeezed (Claude's TUI redrawn at 12 columns). It now neither fits
nor reports a size until shown, attaches with no size, and the server spawns
such an attach at the window's own size (tmux.windowSize). A shown tab
reports its size even when the fit did not change xterm's.
Settings > Agents shows the host's login profiles to staff only: a developer
got two 403s from reads that were never theirs.
Vite's es2020 default made esbuild 0.25 lower `let r; …(r||={})` to
`void 0||(i={})` without the declaration, so every DECRQM query tmux sends
threw "i is not defined" inside the terminal's parser. Found by a headless
smoke run of the preview build.
Dependencies
- Security: body-parser and multer DoS advisories fixed (npm audit: 0).
- Server: Express 5 (query parser kept "extended", req.body defaults to {},
SPA catch-all /{*splat}, send-file helper restores Express 4's dot-folder
rule for sendFile/download, alerts ack route without inline regex),
better-sqlite3 13, js-yaml 5 (server/lib/yaml.js keeps js-yaml 4's default
schema), uuid dropped for crypto.randomUUID, node-pty beta.18, multer 2.5.
- Client: React 19, xterm 6 (WebGL renderer replaces the removed canvas one,
DOM fallback on context loss; Alt+arrow word moves re-mapped), lucide-react 1,
TypeScript 7, i18next/react-i18next/codemirror minors.
Workspace rename step 4
- `agenthub lanes` and `--lane` exit 1 with "renamed: use …"; client api.ts,
the CLI and the MCP server call /api/workspaces; /api/lanes stays mounted.
Server mode (S3): members manage their own Claude Code config
- server/lib/cc-target.js resolves whose config a /cc-config request touches
and gates the router by allowlist; /profiles* and /plugins/update-all stay
admin-only. Member writes go through write-as, CLI calls through sudo as the
member (never -y), reads through openSafe; cc_config_changed reaches only
its owner. Not verified live on an S3 install yet.
Other
- Chat: an HTML file is an artifact only when written whole and not a project
page (index.html, or under src/, public/, templates/, …).
- OpenAPI covers every /api route; openapi-coverage.test.js guards it.
- read-pool test keeps its event loop alive (its worker is unref'd).
- Docs: plan statuses, checklists, CLI.md, LANES.md, SERVER-MODE.md, API.md.
- Today's spend in the status bar and the Claude Code statusline is the
caller's own (`GET /api/pricing/cost?mine=1`, scope.ownSessionWhere), not
every user's sum for an admin or analyst.
- A screenshot pasted into a local claude pane is sent as Claude Code's own
image-paste key: Alt+V on Windows (Ctrl+V did nothing there), ^V elsewhere,
and only when the machine can read its clipboard (wl-paste/xclip + a
display on Linux), else it is uploaded (GET /api/run/native-paste {key}).
- A file manager's files pasted or dropped on a local browser go in by their
own paths, read from the OS clipboard or the file manager's selection
(server/lib/file-manager.js, POST /api/run/clipboard-files and
/selected-files): no upload, no size limit. A copied image file is pasted
by path, text that comes with a picture of it (spreadsheet cells) is
pasted as text, Windows paths go in as `C:/…` mentions.
- Tab titles for Snapshot, Artifacts, Files and Administration.
- An npm tool AgentHub installed under ~/.local is updated there, not by its
own updater into npm's global prefix.
Edited N files (#86): a turn's card counts the files its subagents wrote
or edited (read from their own transcripts, nested ones folded in), and a
turn longer than the chat's first page keeps the edits made before the
page cut. A non-Claude transcript page honours its limit again.
AI tools: OpenCode and Grok Build left the catalog (opencode-usage.js
gone; a Grok payload on the Claude hook route is dropped), and so did the
leftover Kimi Code quota reader.
Artifacts: a list view, skill packs and badges only by exception; a
shared skill follows its owner's folder, and an installer whose copy
differs is offered Update.
A skill in the Hub was a snapshot taken once: the owner changing
~/.claude/skills/<name> changed nothing there, and an installer had to
uninstall and install again to get a newer version.
- syncSkillSources (on GET /api/artifacts, at most every 15 s): a skill row
taken from a folder — the owner's ~/.claude/skills/<name>, or the
workspace's .claude/skills/<name> — is brought up to it. Only a folder
whose names/sizes/mtimes changed is read again, with a pick's checks (no
symlink, no secret, <= 5 MB, a member's files as theirs); a change is a
NEW snapshot, the old one dropped, and an artifact_changed broadcast.
- artifacts.version (snapshot content hash) and source_hash (the folder's
when last taken, additive columns): an upload or a Hub edit records the
folder as it is then, so only a LATER folder change replaces it.
- A list row's updateAvailable: installed, and the viewer's copy differs
from version. The page shows Update (a pack: Update N); install with
{update: true} replaces the copies (old ones moved aside) and is not
counted as another install.
The page showed every artifact as a tall card with a blue Install and four
text actions, a Shared badge on nearly all of them and 12 cards per section,
so 26 artifacts filled ~1900px (5000px on a phone).
- List view by default (Grid kept, a per-browser choice): one row per
artifact — kind icon, name, one line of description, owner and installs,
ONE primary action, everything else in its ⋯ menu / right-click.
- Three or more skills of one owner sharing a name prefix (sg-*, vp-*) are
one pack row: open it for its skills, or Install all / Install N more
after one confirmation (a name already there is left alone).
- Badges only for the exception: Private (not shared), Hub only, Auto.
- No per-section paging: 30 entries, then Show more.
- Scope tabs under the title; category chips scroll on a phone.
- Another user's private row (seen by an admin) gets its own section
instead of sitting under Shared by others.
- "1 installs" → "1 install".
- opencode.json / grok.json leave the built-in catalog; opencode-usage.js,
its poller, transcript branch and table creation go with it (existing
tables and session rows stay as history).
- A Grok payload reaching the Claude hook route (Grok runs the Claude hooks
itself) is dropped instead of recorded as tool = 'grok'.
- quota.js no longer reads Grok or OpenCode Go logins.
- Redone on v2.8.22 from c6b2892 (feat/grok-usage, never merged).
The reported turn (/vp-auto, ~120 messages) showed "Edited 1 file": the
chat loads the newest 50 messages, and the turn's other Write
(rbac_routes_test.go, +176) was in the part before the page.
- turnCarry (agent-edits.js): every message the route's sliding window
drops goes through it; it keeps the file changes since the last prompt it
dropped (a dropped call whose result is on the page, and dropped
subagents, included).
- A Claude page that starts mid-turn answers them as `turn_edits`; the
chat keeps the one from the page that holds its oldest message and adds
it to that turn's card.
Files a turn changed through Bash (python/sed writing a file) are still not
listed: nothing in the transcript says which files a command wrote.
A skill that hands its work to subagents (vp-auto) showed "Edited 1 file"
for a turn that changed 4: a subagent's Write/Edit calls are in its own
agent-<id>.jsonl, so the card only saw the parent's.
- server/lib/agent-edits.js reads a subagent's transcript (readLines, a
member's only as theirs) for the files it wrote or edited with their +/-
lines, nested subagents folded in (3 levels), cached by size + mtime.
- The transcript route puts them on each Claude Agent/Task result as
`edits`; the card adds them to the turn's own edits. A file only a
subagent wrote is listed, never an artifact card (its versions are
rebuilt from this transcript's inputs, which a subagent's are not).
Workspace groups: named, foldable, ordered sidebar sections kept per
account (agenthub.workspace.groups); a workspace joins one from its menu,
a drop on the header, a drag under a row, or Alt+Up/Down past a group's
edge; headers drag to reorder; a folded header shows its most urgent
state; New workspace in this group; fold/unfold all.
Inner repos: GET /api/lanes/:id/repos + ?repo= on git/fs routes; the
Changes panel shows a parent folder's repos only when Settings -> Git ->
Changes panel turns it on (agenthub.changes.innerRepos, off by default).
Cost: the hook writer adds a session's sub-agent transcripts to its
tokens and hours (server/lib/subagent-usage.js) — a sub-agent on the
session's own model was never counted live (~8% of a week here); an
imported session is re-raised on every pass, so a claude -p run first
seen with an empty transcript no longer stays at $0 (~10%). A one-time
raise-only catch-up (user_version 5) re-reads ~/.claude/projects.
Also: omp install script may redirect to its exact install.redirect URL
(#85, repo-links test exempts it), native paste in the terminal.
- Users: today / 7 / 30-day spend read only token_usage_hourly, which holds
Claude transcript hours only — omp/Codex/Antigravity/Cursor sessions,
sub-agent files, baselines and long-context pricing were missing. Now
booked by hourlyTokenRows + calculateCost like the cost routes.
- Update: a page adopts a plugin update the server reports as running
(after a reload, another tab, or one started from an older bundle), and
the progress chip shows whenever an update runs and the modal is not
visible — also when the status stops offering it mid-run.
- Plugin update reports its running step (plugin-update-progress.json →
plugin_update_progress); a hidden update or clone leaves a progress chip.
- Clone here can run in the background.
- Git identity: extra SSH keys are added inline per host (no More keys tab).
- Users: spend today (viewer's local day, ?tz_offset=).
- Prettier on the files touched since v2.8.20.
- A Claude Docs `batch` that creates a doc is captured like an `Artifact`
publish, titled by the doc's name and linked by its own frame URL.
- At Stop/SessionEnd, each pptx/docx/xlsx/pdf/html a workspace session
created or changed is snapshotted into its owner's library (`auto`);
an unchanged file is skipped, a changed one refreshes its snapshot.
- Server mode: a page with no owner carries a suggested owner (the
workspace or home its session ran in). The suggested user claims or
declines it; an admin claims or assigns any. New tab Unowned /
Is this yours? on the Artifacts page; all three actions audited.
- The update dialog can be closed while an update applies: it keeps running,
the page still reloads itself when the new server is up, and a failure
re-opens the dialog with its error.
- Account pool card: one tab per AI tool (Claude Code, Codex, Cursor CLI,
Oh My Pi, and every other installed tool, which says it has no pool),
accounts as cards with 5-hour and weekly quota shown as what is LEFT plus
their reset times, and the saved profiles of Settings → AI tools →
Profiles as cards with "Share to pool".
- Cursor account pool (server/lib/cursor-pool.js): Cursor API keys handed to
a cursor-agent pane only through tmux -e CURSOR_API_KEY;
`agenthub accounts add <name> --tool cursor`.
- Pool groups (server/lib/pool-groups.js, /api/accounts/groups): a group's
members run only on its accounts of each tool, a user in no group on the
accounts no group lists, a pin still wins. account_pins loses its tool
CHECK (rebuilt, pins kept); three new tables.
- Oh My Pi login sharing can be switched from its pool tab too.
- POST /api/me/avatar/url stores the https link (users.avatar_link) instead
of downloading it; GET /api/me/avatar and /api/users/:id/avatar answer 302
to it (no referrer). Link rules: https, no userinfo, <= 2048 chars, no
localhost or private/reserved IP literal. Upload and link replace each other.
- Settings -> Account first loads the link as an embedded image and refuses
one this browser cannot show: a picture behind a login (Mattermost's
/api/v4/users/<id>/image) gets no cookie cross-site and 401s.
- Files sidebar: each workspace under its owner's home, chevrons remembered
per browser (agenthub.files.homesOpen); ownerless ones stay flat.
Chrome gives a PASTE of a file manager's copy (Ctrl+C in Nautilus, Ctrl+V in
the chat) the files and their paths as plain text, but no text/uri-list —
only a drag carries that — so localCandidates found no path and every such
file was uploaded, and refused past the upload limit. Plain-text paths (or
file:// lines) whose names match the pasted files one for one are now
candidates too; sameFilesHere still has the server confirm each is the same
file (size) on this machine, so server mode and remote browsers upload as
before. The prompt box also attaches a file:// text paste with no file by
path, as the terminal already did (copiedFilePaths moved to promptAttach).
- Every Copy goes through copyText: on http://<lan-ip> navigator.clipboard
is undefined, so the push token (shown once), snapshot links, tool paths and
hosts copied nothing, silently or with a TypeError.
- Inputs that submit on Enter use isSubmitEnter, so committing an IME
composition (Vietnamese) no longer saves a half-typed pet name, jumps the
settings search, or submits a cut-off plugin/MCP/marketplace spec.
- LaneChangesPanel, Workspace features/proof and AiToolConfig drop a reply
that lands after the next request started: opening file B before file A's
read returned showed (and let the editor save) A's text under B's header.
- Error-state Retry buttons spin through useSpin; ImageLightbox uses useDialog.
- react-router-dom 6.30 -> 7.18.4 (GHSA-wrjc-x8rr-h8h6, GHSA-337j-9hxr-rhxg);
screen snapshots gain only Link's data-discover attribute.
- StatusBar test follows v2.8.17's "Settings → AI tools → Profiles" text.
Settings → AI tools → Profiles holds one tab per tool: Claude Code (the
cc-profiles panel, moved out of the Claude Code page's modal — its button
now links here), Codex (the whole $CODEX_HOME/auth.json) and Oh My Pi
(`omp auth export` / `omp auth import -`, secrets on stdin; omp's credential
tables are emptied first because import merges, and a failed import puts
the outgoing set back). Antigravity is listed as not switchable: agy keeps
its login in the OS keyring.
server/lib/tool-profiles.js + /api/tool-profiles keep cc-profiles' rules
(one login = one profile, the outgoing login is re-saved before a switch,
an unsaved one refuses it, the live login's profile is not deletable) and
never return a token; /tool-profiles joins ADMIN_WRITE_PREFIXES and
STAFF_ONLY_PREFIXES, writes are same-origin only.
The account pool takes a saved Codex profile too (moved in like a Claude
one, GET /api/accounts/profiles?tool=codex). Each Claude/Codex profile has
a "Move to pool" link (/admin?poolAdd=&poolProfile= opens the pool's Add
dialog on it), Settings shows the pool's size, and the pool card links back.
omp's gateway re-reads its clients file only every 10 s, and an omp that
starts while its key is refused fails model discovery once and hangs for
good. omp-share.js now keeps four keys queued in the clients file (hashes
only; the keys in <data dir>/omp-share/tokens.json, 0600): a new pane takes
one already listed (AGENTHUB_OMP_KEY), a fresh one joins the queue, and a
gone pane's key leaves at the next sync.
- omp-share.js: an admin's switch (S3) runs omp's own auth broker + gateway as
the host on loopback; a member's omp pane reaches it as a scoped client keyed
by its pane token (clients file = live omp panes only), runs on
--model agenthub/<model>, never holds a credential.
- omp catalog: --allow-home, so a workspace at the account's home keeps its
sessions there (they were recorded under a temp dir and dropped).
- Settings → Appearance → Open Markdown as: Preview (default) or Raw, for
Files/Changes, the config viewer and a chat's file panel.
- First-login password change and password rules, avatar URLs, Artifacts
"Mine" includes installed shared skills/plugins, a member's own hook pages.
- Artifacts page laid out like an app store: hero search, category chips,
shelves by kind, app-icon cards with one primary pill and a detail sheet
- POST /api/artifacts/:id/uninstall: a skill's copies in ~/.claude/skills and
~/.agents/skills are moved to ~/.claude/agenthub-replaced-skills/
(-uninstalled-<time>), never deleted; a plugin runs
`claude plugin uninstall --scope user --keep-data` as the caller;
ENOTINSTALLED when nothing is there; allowed while installs are off
- Uninstall on the card, the ⋯ menu and the detail sheet, after a confirm
- The chat over a terminal pane now covers Claude, Codex, omp, Cursor and
Antigravity: Cursor's agent transcript and agy's step log
(brain/<id>/.system_generated/logs/transcript_full.jsonl) are read as
conversations; Cursor sessions are claimed by their workspace
- Replies appear as soon as they are written: the projects-tree watcher
broadcasts `transcript_changed` (at most every 250 ms per session) and the
chat re-reads the transcript while the terminal underneath draws
- Stop shows from the pane's own busy line (Claude/Codex `esc to interrupt`,
agy `… esc to cancel`), or fresh output for omp/Cursor — no hooks needed
- Permission prompts read off the terminal become a card with the tool, its
command and "Deny with a note"; Codex gets Permission mode / Model buttons
that open its own numbered pickers, answered from the menu card
- Artifacts: cards for pages/documents/diagrams/tables/React components, a
Preview | Code panel with versions, copy, download, save to the Artifacts
Hub, full screen; sandboxed iframes without allow-same-origin
- Claude panes get CLAUDE_CODE_ENABLE_TODO_TOOLS=1 so the Progress panel has a
plan; multi-question pane menus read their Next row and question tabs
- The realtime chat bridge (structured stdio, /api/chat-bridge, /ws-chat)
from the unreleased ad9d475 is not kept
- Changes panel: **Discard all** beside Commit discards every non-conflicted
file in one `discard`, after a dialog naming how many untracked files go.
- Terminal: a `claude` pane gets dropped/pasted images as bare paths (shown as
`[Image #n]`) and other files as `@` mentions (`claudePastes`); a local
browser's copied `file://` paths and text-drag file lists paste by path.
- `POST /api/run/drop-file`: a loopback browser outside server mode may drop
up to 2 GB, streamed to disk; remote stays 20 MB.
- cc-config skills/commands/plugins read the USER scope of the account the
pane runs as (S3 member home by `cwd`), never another user's or the host's
for a non-admin without an account; the run form passes its cwd to plugins.
- Chat: an Agent step shows its task, status and tool calls and opens its conversation in place; Windows paths (E:\…, C:/…) in replies open as files
- Prompt: attach a file on this computer by path (native picker), clearer too-large messages
- Artifacts: edit title/description and a skill's files (new snapshot), Reinstall, install history, Hub-only rows, skills installed into ~/.claude/skills and ~/.agents/skills
- Files: batch upload of small files in one extract
- AI Tools: npm install falls back to --prefix ~/.local when the global prefix is not writable
Whole tree from parallel sessions, plus the version bump:
- Clone: a queue with live progress (Add workspace and extra folders),
clones run under nice/ionice so the machine stays responsive.
- Settings → AI tools → Session defaults: a new session's default
permission mode / bypass per account.
- Chat lists the background subagents that have not reported back yet.
- A file pasted or dropped into a claude pane goes in as an @ mention.
- Changes panel: say why amend/undo is refused on a pushed commit.
- openapi.yaml regenerated (Artifacts, Host, resumeById).
The File Explorer's New menu (and its right-click menu) gains Clone
repository: it clones a URL into the folder it has open — of the user's own
home or of a workspace's own folder — through POST /api/lanes/clone with
that folder as `parent`, so the URL allowlist, the own-folder check and the
folder account's own git identity all apply, and opens a workspace on the
clone. ECREDENTIALS asks for a login, ESSHAUTH points to Settings → Git.
GET /api/homes now gives `me` its absolute `dir`. Keys typed in the dialog
never reach the explorer's shortcuts (Backspace / Delete).
- omp-usage.js / opencode-usage.js: poll each CLI's own session store into
sessions (tool omp/opencode), token_usage per model and subagent agents;
transcript routes read them (omp journals, OpenCode's opencode.db).
- Grok runs the Claude hooks in ~/.claude/settings.json: its hooks are now
recognised (hookEventName + workspaceRoot), tagged tool=grok, and their
transcript_path dropped instead of being parsed as a Claude transcript.
- Catalog `resumeById`: omp/Grok/Cursor `--resume <id>`, OpenCode
`--session <id>`; session ids may carry `_`. The client offers Resume for
any tool that resumes by id.
- Kimi Code and GitHub Copilot leave the catalog (their session stores could
not be verified).
The workspace's "Or clone repositories" box now reads GET /api/me/git and
checks every URL as it is typed: an SSH link while the account has no SSH
key is never sent and points to Settings → Git (make a key, add its public
key to the host); an HTTPS host with no saved login is flagged with an
optional login row, since a public repo needs none. The identity is re-read
when the window regains focus, and a refused SSH login links to the same
settings. The server's ECREDENTIALS / ESSHAUTH answers still decide.
Each account row in the Account pool card has a pencil: tick the users
whose sessions run on it (one moves off any other account, unticking sends
one back to the balancer). PUT /api/accounts/:name/users saves the set in one
transaction, audits each change and switches a Claude login into each added
user's home at once. The card-level switch button and pinned list are gone.
PUT /api/accounts/pins (and agenthub accounts pin) now writes a pinned
Claude login into the user's home right away (claude-pool.switchHome):
every home's newer pair is copied back first, the member's own login is
never written over, and no pane is restarted — running sessions take the
new pair at their next credentials read. A live home stays on the login it
holds, so unpinning does not flip it back. The Users page calls it
"Switch a user's account" and says what happened.
An admin pins a server-mode user to one pool account per tool
(account_pins; PUT /api/accounts/pins with the password step-up and audit,
the Users page's Pin a user, agenthub accounts pin|unpin). The workspace
owner's pin wins over the balancer whenever the pane may use the account;
a pinned login still waits while another pool login holds the home.
v2.8.9 crashed at start on every plugin install with "Cannot find module
'js-yaml'": server/lib/lane-detect.js requires it, but it was only a
devDependency that redoc used to pull in, and v2.8.8 dropped redoc. The
plugin installs with --omit=dev. This box hid it: Debian's global
/usr/share/nodejs has js-yaml. Reproduced with a pruned install and the
global paths off; fixed by moving js-yaml (and argparse in the lock) to
dependencies. New runtime-deps.test.js fails on any server/ require that is
not a dependency.
- Sagi: clicking the pet opens its chat window directly (PetChat + SagiInfo
status strip, Needs you, Mute / Clear alerts); SagiPanel removed.
- Chat view answers Claude's unnumbered folder-trust prompt (parseBareMenu).
- pty-attach: resize/write after a Windows attach exited, and node-pty's pipe
errors, no longer throw out of the WebSocket handler.
- restart-helper: the server started by an update logs to
data/agenthub-server.log instead of discarding its output.