feat: v2.5.12 — plan and quota for every AI tool, Grok Build and Kimi Code, Quit AgentHub (#65)
Usage panel (status bar): - every account shows its plan (Claude subscriptionType, Codex chatgpt_plan_type, Cursor membershipType, Grok subscriptionTier) - new quota rows: Cursor CLI (cursor.com usage-summary), Antigravity (`agy --print /usage`, two model pools; AGENTHUB_AGY_USAGE=0 never runs agy), Grok Build, Kimi Code, OpenCode Go — each login read from the tool's own file, sent only to its own host while valid, never refreshed - in-flight quota reads are shared between tabs AI Tools catalog: Grok Build (`grok`) and Kimi Code (`kimi`) added; Gemini CLI, Hermes and OpenClaw removed. Also: Quit AgentHub (#65, `agenthub stop`), status-area icon, Add to Chat on an editor selection, Settings in three groups, JetBrains-style Changes panel (ticked files/hunks, ignore, blame, file history), terminal font. --no-verify: :3000 is taken by another dev server (lane-detect tests); both suites run by hand — server 2636/2639 (the 3 are :3000), client 1131/1131.
This commit is contained in:
+54
-13
@@ -85,11 +85,13 @@ A dashboard shared by several people runs in **server mode** ([SERVER-MODE.md](S
|
||||
| `POST /api/auth/login` `{username, password}` | `{user}` + the cookie; `401 EBADLOGIN`, `429 ETOOMANY`, `404 ENOTSERVER` in local mode |
|
||||
| `POST /api/auth/logout` | `{ok: true}`, cookie cleared |
|
||||
|
||||
In server mode a **member** sees only rows of the workspaces they own (`lanes.owner_id`): every list, count, chart and cost answer is limited to them, and a request naming another user's session, agent, workspace or run (in the path, query or body) answers `404 ENOTFOUND`. Admin and analyst see everything; `GET /api/users` lists every account with its workspaces, sessions and 7/30-day `{cost, tokens}`, and `GET /api/users/audit[?limit=]` the staff reads of members' sessions (both `403 EADMIN` for a member). See [SERVER-MODE.md](SERVER-MODE.md#what-each-user-sees).
|
||||
In server mode a **member** sees only rows of the workspaces they own (`lanes.owner_id`): every list, count, chart and cost answer is limited to them, and a request naming another user's session, agent, workspace or run (in the path, query or body) answers `404 ENOTFOUND`. Admin and analyst see everything; `GET /api/users` lists every account with its workspaces, sessions and 7/30-day `{cost, tokens}`, and `GET /api/users/audit[?limit=]` the staff reads of members' sessions and admins' account changes (both `403 EADMIN` for a member). See [SERVER-MODE.md](SERVER-MODE.md#what-each-user-sees).
|
||||
|
||||
An admin changes accounts with `POST /api/users` (`{username, password, role}` → `201 {user}`), `PATCH /api/users/:id` (`{role?, disabled?}`), `POST /api/users/:id/password` (`{password}`; that user's logins end) and `POST /api/users/:id/workspaces` (`{laneId}`; the workspace's owner becomes that user). Every one carries `currentPassword`, the admin's own password: `403 EREAUTH` when it is wrong or missing, `429 ETOOMANY` after 10 wrong tries in 15 minutes. `409 ESELF` for an admin's own role or disable, `409 EEXISTS` for a taken name, `400 EBADUSER`/`EBADPASSWORD`/`EBADROLE`, `404 ENOUSER`/`ENOLANE`; an analyst gets `403 EREADONLY`, a member `403 EADMIN`, and local mode `403 ELOCALMODE`. No answer carries a password hash; each change is written to the audit log.
|
||||
|
||||
`GET /api/accounts[?force=1]` lists the Claude account pool ([SERVER-MODE.md](SERVER-MODE.md#claude-account-pool)): `{accounts: [{name, addedAt, expiresAt, expired, livePanes, spend5h, spend7d, quota: {fiveHour, sevenDay, checkedAt, error} | null, quotaPercent, full}], blockPercent}`. It never includes a token. `force=1` re-reads every quota first. Run rows (`/api/run/history`) carry `account`, the pool account their pane ran on (`null` = the machine's own login).
|
||||
|
||||
`GET /api/settings/info` includes `server.version` (the running dashboard release). Pair with `agenthub version` or the Settings About panel to confirm client and server builds match after deploy.
|
||||
`GET /api/settings/info` includes `server.version` (the running dashboard release). Pair with `agenthub version` or Settings → System to confirm client and server builds match after deploy.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
@@ -580,6 +582,7 @@ that file.
|
||||
|
||||
```http
|
||||
GET /api/lanes/:id/git/log?limit=30[&file=<path>][&ref=<branch>]
|
||||
GET /api/lanes/:id/git/blame?path=<path>
|
||||
GET /api/lanes/:id/git/file?path=<path>[&rev=work|HEAD|<sha>]
|
||||
PUT /api/lanes/:id/git/file {path, content, baseSha}
|
||||
GET /api/lanes/:id/git/raw?path=<image>[&rev=work|HEAD|<sha>]
|
||||
@@ -596,10 +599,16 @@ answer `{available:false}` with HTTP **200** for a cwd git cannot read.
|
||||
refs, parents}]}`, newest first in `--date-order` (no parent before its
|
||||
children; `parents` = parent shas, first parent first), `limit` clamped to 200. `file=` narrows it to the
|
||||
commits touching that path — relative, no `..` segment, else
|
||||
`400 EBADPATH`. `ref=` reads another branch's history instead of HEAD's
|
||||
`400 EBADPATH` — with each `parents` REWRITTEN to the file's own previous
|
||||
commit (`--parents`), so the file's graph is one joined-up line. `ref=` reads another branch's history instead of HEAD's
|
||||
(where the panel's Cherry-pick finds its commits): a local or
|
||||
remote-tracking branch that exists, else `400 ENOBRANCH`. Reading one commit's diff is
|
||||
`/git/changes?range=commit&sha=`.
|
||||
- **`/git/blame`** → `{available, path, commits: {<sha>: {short, author,
|
||||
date, summary}}, lines: [<sha>|null]}` — `git blame --porcelain` of the
|
||||
working-tree file (Annotate); `null` = a line not committed yet. Same
|
||||
lexical guard as `/git/file`, `403 ESECRET`, `400 ETOOBIG` over 1 MB,
|
||||
`400 ENOFILE`; an untracked file answers `{available:false}`.
|
||||
- **`PUT /git/file`** saves the whole-file editor (same-origin guarded;
|
||||
`server/lib/lane-file-write.js`). Same path guards as the read, plus: the
|
||||
file must already exist, be text and within the 1 MB cap, and `baseSha`
|
||||
@@ -689,12 +698,15 @@ POST /api/lanes/:id/git/commit-message
|
||||
|
||||
The Changes panel's ✨ button beside the commit box. Runs the user's own
|
||||
`claude` CLI — `claude -p --safe-mode --no-session-persistence --tools "" --model haiku`
|
||||
— on the **staged** diff of the workspace's cwd (or `?session=` worktree), sent on
|
||||
— on the **staged** diff of the workspace's cwd (or `?session=` worktree) — or,
|
||||
with a body `{"paths": [...]}` (the panel's ticked files), on those files'
|
||||
working-tree diff against HEAD, untracked ones included — sent on
|
||||
stdin together with the last ten commit subjects so the message follows the
|
||||
repo's style. Only reads: no tool, hook or plugin runs, no transcript is saved,
|
||||
and a `settings.local.json` is never part of the diff. Same-origin, because
|
||||
each call spends the user's Claude quota. Answers `{"message": "..."}`;
|
||||
`400 ENOSTAGED` with nothing staged, `400 ENOTOOL` without `claude` on PATH,
|
||||
`400 ENOSTAGED` with nothing staged (or no readable ticked file), `400 EBADPATH`
|
||||
for a path git does not report changed, `400 ENOTOOL` without `claude` on PATH,
|
||||
`502 EAI` when the CLI fails, answers empty or runs past 120 s.
|
||||
|
||||
#### Act on a workspace's working copy
|
||||
@@ -718,7 +730,7 @@ cannot collide on `index.lock`.
|
||||
| `discard` | `git restore --source=HEAD --staged --worktree` (tracked) / `git clean -fd` (untracked) | `{"paths": ["..."], "confirm": true}` |
|
||||
| `stage-hunk` | `git apply --cached` of ONE hunk of `git diff -U<context> HEAD -- <path>` — or, with `block`, of only that hunk's `block`-th run of +/- lines (the other runs folded into context, as an edited `git add -p` hunk) | `{"path": "...", "hunk": "@@ -1,2 +1,3 @@", "block": 0, "context": 3}` |
|
||||
| `discard-hunk` | `git apply -R` of that one hunk (or that one `block` of it), off the working tree | `{"path": "...", "hunk": "@@ ... @@", "block": 0, "context": 3, "confirm": true}` |
|
||||
| `commit` | `git add -A` then `git commit -m <message>`, or the index as it stands with `only: "staged"` | `{"message": "...", "only": "staged"}` |
|
||||
| `commit` | `git add -A` then `git commit -m <message>`, or the index as it stands with `only: "staged"`, or — with `paths` — `git commit --only -- <paths>`: exactly those files' working-tree content (an untracked one is added first, and taken back out if the commit fails), every other change left as it was | `{"message": "...", "only": "staged"}` / `{"message": "...", "paths": ["..."]}` |
|
||||
| `switch` | `git switch [-c] <branch>` | `{"branch": "...", "create": false}` |
|
||||
| `branch-create` | `git branch <branch> [<base>]` — never switches | `{"branch": "...", "base": "..."}` |
|
||||
| `branch-delete` | `git branch -d <branch>` | `{"branch": "..."}` |
|
||||
@@ -730,12 +742,24 @@ cannot collide on `index.lock`.
|
||||
| `cherry-pick` | `git cherry-pick <sha>` — a non-merge commit, onto a clean repo-root workspace | `{"sha": "9b3e74a"}` |
|
||||
| `revert` | `git revert --no-edit <sha>` — same guards as `cherry-pick` | `{"sha": "9b3e74a"}` |
|
||||
| `undo-commit` | `git reset --soft HEAD~1` — only an UNPUSHED, non-merge, non-root HEAD that is still `sha`; its changes stay staged | `{"confirm": true, "sha": "<HEAD the user saw>"}` |
|
||||
| `amend` | `git commit --amend -m <message>`, or `--no-edit` with no message — only an UNPUSHED HEAD that is still `sha`; takes the index as it stands | `{"message": "...", "sha": "<HEAD>"}` |
|
||||
| `amend` | `git commit --amend -m <message>`, or `--no-edit` with no message — only an UNPUSHED HEAD that is still `sha`; takes the index as it stands, or with `paths` exactly those files (`--only`; an empty list rewords and needs a message) | `{"message": "...", "sha": "<HEAD>", "paths": ["..."]}` |
|
||||
| `ignore` | appends one anchored, escaped `/<path>` line per path to the WORKSPACE's own `.gitignore` (created if missing, never a line it already has) — untracked paths only (`400 ETRACKED`), never through a symlink or onto a non-regular file (`400 EBADPATH`) | `{"paths": ["..."]}` |
|
||||
|
||||
```json
|
||||
{ "action": "commit", "ok": true, "output": "[feat/x 9b3e74a] fix: the thing" }
|
||||
```
|
||||
|
||||
- `commit`/`amend` also take `hunks` — `{"<path>": [{"hunk": "@@ … @@", "blocks": [0]}]}`
|
||||
with `context` — to commit only those change blocks of a tracked,
|
||||
non-renamed file (alone or beside whole `paths`): the commit is built in a
|
||||
temporary index from git's own fresh diff (never text from the request) and
|
||||
the real index entries of the committed paths are reset to the new HEAD
|
||||
afterwards, the rest left as an unstaged change. A hunk no longer in the diff
|
||||
is `409 EHUNKGONE`, an untracked path `400 EUNTRACKED`, a malformed entry
|
||||
`400 EBADPATH`.
|
||||
- `commit`/`amend` with `paths` refuse a conflicted path (`409 ECONFLICTS`) —
|
||||
a merge is finished by `merge-commit` — and any path git does not report
|
||||
changed (`400 EBADPATH`).
|
||||
- `pull` is **always** `--ff-only` and `switch` refuses a dirty tree
|
||||
(`400 EDIRTY`): neither can invent a merge commit or a conflict underneath a
|
||||
running agent. There is deliberately no rebase, hard reset or stash here —
|
||||
@@ -843,7 +867,7 @@ DELETE /api/git-credentials/:protocol/:host Delete one (404 ENOTFOUND)
|
||||
|
||||
AgentHub's OWN git logins, one per `protocol + host`, shared by everyone using
|
||||
this install; the **Git credentials** tab of Settings
|
||||
(`/settings/git-credentials`) and the git bar's login form both write
|
||||
(`/settings/git`) and the git bar's login form both write
|
||||
here. Stored in `<CLAUDE_HOME>/agenthub-git-credentials/credentials.json`
|
||||
(dir `0700`, file `0600`). **No response ever carries a password.** Writes are
|
||||
same-origin guarded; a bad protocol/host is `400 EBADHOST`, an empty login
|
||||
@@ -1786,7 +1810,24 @@ GET /api/settings/autostart
|
||||
PUT /api/settings/autostart
|
||||
```
|
||||
|
||||
Settings → About's **Start AgentHub automatically** switch. `GET` answers `{ available, kind, enabled, foreign }`: `kind` is `systemd` (a `--user` unit) or `launchd` (a LaunchAgent), `available` is false with no systemd `--user` / no macOS GUI login, `foreign` is true when a unit named `agenthub` exists that AgentHub did not write. `PUT { enabled: boolean }` (same-origin only) enables or disables the unit — writing one for the running server first when none exists — and answers the same shape. It never starts or stops the server. Off by default: a plugin install gets no unit until this is turned on. `400 EBADVALUE` for a non-boolean, `400 EUNAVAILABLE` with no service manager, `409 EFOREIGN` for a foreign unit.
|
||||
Settings → System's **Start AgentHub automatically** switch. `GET` answers `{ available, kind, enabled, foreign }`: `kind` is `systemd` (a `--user` unit) or `launchd` (a LaunchAgent), `available` is false with no systemd `--user` / no macOS GUI login, `foreign` is true when a unit named `agenthub` exists that AgentHub did not write. `PUT { enabled: boolean }` (same-origin only) enables or disables the unit — writing one for the running server first when none exists — and answers the same shape. It never starts or stops the server. Off by default: a plugin install gets no unit until this is turned on. `400 EBADVALUE` for a non-boolean, `400 EUNAVAILABLE` with no service manager, `409 EFOREIGN` for a foreign unit.
|
||||
|
||||
#### Status-area icon
|
||||
|
||||
```http
|
||||
GET /api/settings/tray
|
||||
PUT /api/settings/tray
|
||||
```
|
||||
|
||||
Settings → System's **Show AgentHub icon in the status area** switch (`server/lib/tray.js`). `GET` answers `{ available, enabled, running }`: `available` is false in server mode and on a Linux machine with no graphical session (`DISPLAY`/`WAYLAND_DISPLAY`, read from `systemctl --user show-environment` too), `running` is true while the icon process heartbeats. `PUT { enabled: boolean }` (same-origin only) removes or writes `<data dir>/runtime/tray-off` and starts or stops the icon process at once; it never touches the server. On by default. `400 EBADVALUE` for a non-boolean.
|
||||
|
||||
#### Quit AgentHub
|
||||
|
||||
```http
|
||||
POST /api/settings/quit
|
||||
```
|
||||
|
||||
Settings → System's **Quit AgentHub** and `agenthub stop` (issue #65; same-origin only, admin-only in server mode). Writes `<data dir>/runtime/stopped`, kills every `agenthub-lane-*` tmux session (their runs are recorded `killed`), answers `{ ok: true, killed }` and then stops the server: through `systemctl --user stop --no-block agenthub.service` / `launchctl bootout` when AgentHub's unit is running it (a SIGTERM would be restarted by `Restart=always`/`KeepAlive`), else by SIGTERM. While the flag exists the plugin bootstrap starts no server; every server clears it when it starts listening, so a boot/login through an enabled autostart unit brings the dashboard back.
|
||||
|
||||
---
|
||||
|
||||
@@ -2068,7 +2109,7 @@ curl -X DELETE http://localhost:4820/api/pricing/gpt-5.1-codex
|
||||
GET /api/pricing/cost?tz_offset=<minutes>&sources=<csv>&lane=<id>&tool=<id>&from=<YYYY-MM-DD>&to=<YYYY-MM-DD>&date=<YYYY-MM-DD>
|
||||
```
|
||||
|
||||
The total cost with a per-model `breakdown`, plus `daily_costs: [{date, cost}]` by local date (`tz_offset` is the browser's `getTimezoneOffset()`). `date=<YYYY-MM-DD>` prices only that local day — `total_cost`, `breakdown`, `by_tool` and `daily_costs` all cover just it (any other shape is `400 INVALID_INPUT`); the status bar's **Today** asks for the local date and shows the day's token total beside it, listing tokens and cost per model on hover (as it does for **Workspace** and **Session**). A day is the day the tokens were SPENT: each session's total is split over the local days of its `token_usage_hourly` rows (one per UTC hour, from each Claude transcript message's timestamp), and whatever the total holds beyond those rows — a session recorded before that table and whose transcript is gone, sub-agent files, Codex and Antigravity sessions — is booked to the session's start date. The days always add up to the total. `lane=<id>` narrows it to the sessions that workspace owns (`sessions.lane_id`, or held by one of its `dashboard_runs` and not stamped yet — the rule of `GET /api/sessions?lane=`; a non-numeric id is `400 INVALID_INPUT`); the status bar shows it as **Workspace $…** on the Workspace page, beside **Session $…** from `GET /api/pricing/cost/:sessionId` for the replayed session or the focused pane's. `tool=<id>` narrows it to one AI tool's sessions (`sessions.tool`), and `by_tool: [{tool, cost}]` (most expensive first) splits the total per tool — each tool priced on its own rows, so the code-execution free allowance applies per tool. A Codex turn on the fast/priority service tier is booked as `speed = fast` (priced with the model's priority rates, which the pricing sync writes into the fast columns), one on the flex tier as `service_tier = flex` (half price, like batch).
|
||||
The total cost with a per-model `breakdown`, plus `daily_costs: [{date, cost}]` by local date (`tz_offset` is the browser's `getTimezoneOffset()`). `date=<YYYY-MM-DD>` prices only that local day — `total_cost`, `breakdown`, `by_tool` and `daily_costs` all cover just it (any other shape is `400 INVALID_INPUT`); the status bar's **Today** asks for the local date and shows the day's token total beside it, listing tokens and cost per model on hover (as it does for **Workspace** and **Session**). A day is the day the tokens were SPENT: each session's total is split over the local days of its `token_usage_hourly` rows (one per UTC hour, from each Claude transcript message's timestamp), and whatever the total holds beyond those rows — a session recorded before that table and whose transcript is gone, sub-agent files, Codex and Antigravity sessions — is booked to the session's start date. The days always add up to the total. `lane=<id>` narrows it to the sessions that workspace owns (`sessions.lane_id`, or held by one of its `dashboard_runs` and not stamped yet — the rule of `GET /api/sessions?lane=`; a non-numeric id is `400 INVALID_INPUT`); the status bar shows it as **Workspace $…** on the Workspace page, beside **Session $…** from `GET /api/pricing/cost/:sessionId` for the replayed session or the focused pane's. `tool=<id>` narrows it to one AI tool's sessions (`sessions.tool`), and `by_tool: [{tool, cost}]` (most expensive first) splits the total per tool — each tool priced on its own rows, so the code-execution free allowance applies per tool. A Codex turn on the fast/priority service tier is booked as `speed = fast` (priced with the model's priority rates, which the pricing sync writes into the fast columns), one on the flex tier as `service_tier = flex` (half price, like batch). A Claude request whose whole prompt (input + cache read + cache write) is above 100k tokens is also counted into the bucket's `long_*_tokens` (returned on each `breakdown` entry) and that part is priced at the rule's `long_input_per_mtok` / `long_output_per_mtok` (Haiku 5.5: 5x; cache rates scale by the input ratio; a rule with no long rates prices it at its standard rates).
|
||||
|
||||
`from` / `to` (local dates, inclusive, read with `tz_offset`; either end may be left open) narrow `total_cost`, `breakdown`, `by_tool`, `feature_costs` and `unpriced_models` to the tokens SPENT on those days (the same split as `daily_costs`). `daily_costs` itself stays the whole series, so a 30-day trend or a month-to-date pace is unaffected by the range. A malformed date, a date from year 9999, or `from` after `to` is `400 INVALID_INPUT`.
|
||||
|
||||
@@ -2083,7 +2124,7 @@ curl "http://localhost:4820/api/pricing/cost?tz_offset=-420&from=2026-09-01&to=2
|
||||
GET /api/pricing/quota[?force=1]
|
||||
```
|
||||
|
||||
How much of each native tool's plan quota is spent: `{ tools: [{ id: "claude"|"codex", active, label?, account?, expiresAt?, windows: [{ label, usedPercent, resetsAt }], error?, asOf? }], checkedAt }` (`server/lib/quota.js`). **Claude** asks Anthropic's `GET /api/oauth/usage` with the access token the CLI holds right now — never refreshed, so an expired token answers `error: "ELIVEEXPIRED"` until Claude Code refreshes it — cached 5 min (`force=1`, the status bar's click, re-asks at most every 30 s); no OAuth login, no entry. That live account is `active: true`; every OTHER saved auth profile follows as `active: false` with its `label`, asked with its OWN stored token only while that token is valid — never refreshed here, since a refresh rotates the saved credentials (only the Profiles modal's **Check quota** does that) — and, once expired, showing the last figure this server read (`asOf`) or `error: "EPROFILEEXPIRED"`. Each Claude token's owner is also asked (`GET /api/oauth/profile`): the live row carries the label and email of the account its token belongs to (`.claude.json` can name another), and a saved profile whose token belongs to a different account answers `error: "EWRONGACCOUNT"` with no windows, never that other account's usage. **Codex** reads the newest `rate_limits` from its rollout journals, no network (`asOf` = when the journal recorded it; a window past its reset reads 0). `account` is the signed-in email and `expiresAt` when the stored access token runs out — Claude's `claudeAiOauth.expiresAt`, Codex's `auth.json` access-token `exp` (JWT claims read unverified; no token leaves the server). Labels: `5h`, `wk`, `mo`, else `<n>d`/`<n>h` (the hover spells `wk`/`mo` out as week/month). The status bar shows one "N% used" chip per tool (its live account) at its right end and, on hover, every account grouped per tool with a row per window — bar, percent, reset time.
|
||||
How much of each native tool's plan quota is spent: `{ tools: [{ id: "claude"|"codex"|"antigravity"|"cursor"|"grok"|"kimi"|"opencode", active, label?, account?, plan?, expiresAt?, windows: [{ label, usedPercent, resetsAt, group? }], error?, asOf? }], checkedAt }` (`server/lib/quota.js`). **Claude** asks Anthropic's `GET /api/oauth/usage` with the access token the CLI holds right now — never refreshed, so an expired token answers `error: "ELIVEEXPIRED"` until Claude Code refreshes it — cached 5 min (`force=1`, the status bar's click, re-asks at most every 30 s); no OAuth login, no entry. That live account is `active: true`; every OTHER saved auth profile follows as `active: false` with its `label`, asked with its OWN stored token only while that token is valid — never refreshed here, since a refresh rotates the saved credentials (only the Profiles modal's **Check quota** does that) — and, once expired, showing the last figure this server read (`asOf`) or `error: "EPROFILEEXPIRED"`. Each Claude token's owner is also asked (`GET /api/oauth/profile`): the live row carries the label and email of the account its token belongs to (`.claude.json` can name another), and a saved profile whose token belongs to a different account answers `error: "EWRONGACCOUNT"` with no windows, never that other account's usage. **Codex** reads the newest `rate_limits` from its rollout journals, no network (`asOf` = when the journal recorded it; a window past its reset reads 0). **Antigravity** runs the catalog-detected `agy` as `agy --print /usage --output-format json` — the CLI's own `/usage` command, no model call (`num_turns` 0, no conversation) — in a private empty temp directory with a 60 s timeout, only while the tool is enabled in AI Tools and `agy --version` is ≥ 1.1.11 (an older agy may bill `/usage` as a model request); cached like Claude, a failed run shows the last figure (`asOf`) or `error: "EUPSTREAM"`. One window per pool bucket (`5h`, `wk`), `group` naming the pool ("Gemini Models", "Claude and GPT models"); the report names no account and no plan. Running agy may let it update itself; `AGENTHUB_AGY_USAGE=0` never runs it (no entry). **Cursor** reads the Cursor CLI's (`cursor-agent`) own login, `$XDG_CONFIG_HOME/cursor/auth.json` (default `~/.config`), and sends it ONLY to cursor.com's undocumented `GET /api/usage-summary` as the `WorkosCursorSessionToken=<user id>::<token>` cookie (user id from `~/.cursor/cli-config.json` `authInfo`), while its JWT is valid and never refreshed — expired or a 401 is `error: "ETOKENEXPIRED"`; cached like Claude; no file (macOS keeps it in the keychain), no entry. One `mo` window: `individualUsage.plan.totalPercentUsed`, resetting at `billingCycleEnd` (an Enterprise seat without `plan` shows its plan name and no window). **Grok Build** reads the CLI's `auth.x.ai` session in `$GROK_HOME/auth.json` (default `~/.grok`) and sends it only to `cli-chat-proxy.grok.com`'s `GET /v1/billing?format=credits` (`wk` = `creditUsagePercent`, `mo` = `used / monthlyLimit`). **Kimi Code** reads `$KIMI_CODE_HOME/credentials/kimi-code.json` (default `~/.kimi-code`) and sends it only to `api.kimi.com`'s `GET /coding/v1/usages` (`5h`/`wk`/`mo` from `limit_5h`/`limit_7d`/`limit_month_total`). Both are used only while valid and never refreshed (`error: "ETOKENEXPIRED"`; a Kimi refresh would rotate the CLI's refresh token and sign it out); no file, no entry. **OpenCode Go** (`id: "opencode"`, `plan: "Go"`) reads opencode's own `opencode-go` API key — `auth.json`, else the `credential` table of `opencode.db`, under `$XDG_DATA_HOME/opencode` (default `~/.local/share`) — and sends it only to `opencode.ai`'s `GET /zen/go/v1/usage` (`5h`/`wk`/`mo` meters); an opencode with no Go key has no entry. These readers follow Orca's (MIT). All three cached like Claude. `plan` is the subscription's name as the tool reports it — Claude's `subscriptionType`, Codex's `chatgpt_plan_type` id-token claim, Cursor's `membershipType`, Grok's `subscriptionTier` — shown as a badge beside the account. `account` is the signed-in email and `expiresAt` when the stored access token runs out — Claude's `claudeAiOauth.expiresAt`, Codex's and Cursor's access-token `exp` (JWT claims read unverified; no token leaves the server but Cursor's own, to cursor.com). Labels: `5h`, `wk`, `mo`, else `<n>d`/`<n>h` (the hover spells `wk`/`mo` out as week/month). The status bar shows one "N% used" chip per tool (its live account) at its right end and, on hover, every account grouped per tool with a row per window — bar, percent, reset time.
|
||||
#### Analytics
|
||||
|
||||
```http
|
||||
@@ -2307,7 +2348,7 @@ Pulls history from **every enabled** source sequentially (one SSH connection at
|
||||
|
||||
#### Push ingest (`POST /api/hooks/ingest-batch`)
|
||||
|
||||
For a machine the dashboard cannot reach over SSH: it pushes its own hook payloads. Off until **Settings → Data → Push ingest** creates a token (`POST /api/settings/push-token`, returned once; only its SHA-256 is stored as `AGENTHUB_PUSH_TOKEN_SHA256`).
|
||||
For a machine the dashboard cannot reach over SSH: it pushes its own hook payloads. Off until **Settings → Database → Push ingest** creates a token (`POST /api/settings/push-token`, returned once; only its SHA-256 is stored as `AGENTHUB_PUSH_TOKEN_SHA256`).
|
||||
|
||||
```http
|
||||
POST /api/hooks/ingest-batch
|
||||
@@ -2567,7 +2608,7 @@ DELETE /api/pets/:id Delete a pet (its folder under the data di
|
||||
|
||||
The mascot (Sagi) can wear a pet: right-click it for **Hide pet**, **Size** (48–250 px), **Choose pet** (Sagi, the featured pets, your own — deletable), **Upload your own…**, **Import .codex-pet bundle…**, **Find more on codex-pets.net** and **Pet settings…**, which opens the pet block of **Settings → Appearance** (`/settings/appearance#pet`, `client/src/components/settings/PetSettings.tsx`; the old `/settings/pet` redirects there): show/hide, speech bubbles, size with a live preview, and the same choose / download / upload / import / delete as a grid. The menu and the page share `components/Sagi/usePets.ts`. Which pet is worn and its size are viewer preferences in `localStorage` (`agenthub.pet`, `agenthub.pet.size`); the pets live on the server, one folder each under `<data dir>/pets/<id>/` with a `meta.json` AgentHub writes.
|
||||
|
||||
A **Codex pet** is the `.codex-pet.zip` format Codex Desktop uses (the bundles shared on codex-pets.net and in `mileson/codex-pets`): `pet.json` (`id`, `displayName`, `description`, `spritesheetPath`) and `spritesheet.webp`, a 1536×1872 atlas of 8 columns × 9 rows of 192×208 frames — idle, run right, run left, waving, jumping, failed, waiting, running, review, with 6/8/8/4/5/8/6/6/6 frames. The mascot plays the row of its mood (an agent working → running, waiting for you → waiting, an error → failed, a finished turn → jumping, idle/asleep → idle); dragged it runs left or right with the drag (and along the glide to its edge), hovered it waves. Each row keeps its own pace with a longer hold on its last frame (idle ~220 ms a frame, then ~1.4 s). Only the two files are read, at the archive root or one folder deep; an entry's size is checked from the zip's directory before it is inflated; the sheet must be a WebP of exactly that size (`400 EBADPET` otherwise). An uploaded image is shown still and bobs; SVG is refused because an SVG served from the dashboard's origin runs script.
|
||||
A **Codex pet** is the `.codex-pet.zip` format Codex Desktop uses (the bundles shared on codex-pets.net and in `mileson/codex-pets`): `pet.json` (`id`, `displayName`, `description`, `spritesheetPath`) and `spritesheet.webp`, a 1536×1872 atlas of 8 columns × 9 rows of 192×208 frames — idle, run right, run left, waving, jumping, failed, waiting, running, review, with 6/8/8/4/5/8/6/6/6 frames — or a v2 atlas (`spriteVersionNumber: 2`, 1536×2288, 11 rows) whose rows 9–10 add 16 gaze poses, 22.5° apart clockwise from straight up. The mascot plays the row of its mood (an agent working → running, waiting for you → waiting, an error → failed, a finished turn → jumping, a live session at rest → review, idle/asleep → idle); dragged it runs left or right with the drag (and along the glide to its edge), hovered it waves, and a v2 pet at idle turns toward the pointer (back to idle 2 s after the pointer rests). Frames play at Codex's own timings (`codex-rs/tui/src/pets/model.rs`: e.g. running 120 ms a frame, its last 220 ms); idle plays at the drawn pace (280/110/110/140/140/320 ms), a sleeping pet at Codex's 6× slower one. Waving, jumping, failed and review are moments — they play three times and settle into idle; running, waiting and the run rows loop. Only the two files are read, at the archive root or one folder deep; an entry's size is checked from the zip's directory before it is inflated; the sheet must be a WebP of exactly one of those sizes (`400 EBADPET` otherwise). An uploaded image is shown still and bobs; SVG is refused because an SVG served from the dashboard's origin runs script.
|
||||
|
||||
**Featured pets are downloaded, never bundled.** The three offered (Goku, Border Collie, 11) are other people's uploads to codex-pets.net with no licence to redistribute them, so AgentHub ships none of their art: `POST /api/pets/featured/:id` takes only an id from the fixed list in `server/lib/pets.js` (`404 ENOPET` for any other, with no request made) and fetches `https://codex-pets.net/api/pets/<id>` and `/spritesheet` — no URL from the request, no redirects followed. A pet id is always made by the server (`p-<12 hex>` for an upload, `cp-<featured id>`), every write is same-origin (`403 EBADORIGIN`), and at most 50 pets are kept (`409 ETOOMANY`). Other errors: `413 ETOOBIG`, `502 EFETCH` (codex-pets.net unreachable or refusing).
|
||||
|
||||
|
||||
Reference in New Issue
Block a user