feat: account pool per tool with groups and a Cursor pool; update runs in the background

- 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.
This commit is contained in:
2026-10-22 17:02:08 +07:00
parent 44426faf16
commit 685eb748e8
30 changed files with 2891 additions and 299 deletions
+229 -4
View File
@@ -3,7 +3,7 @@
openapi: 3.0.3
info:
title: Agent Dashboard for Claude Code API
version: 2.8.19
version: 2.8.20
description: HTTP API for real-time Claude Code session monitoring, agent lifecycle tracking, analytics, pricing, hooks ingestion, and workflow intelligence.
contact:
name: Nguyễn Ngọc Trí Vĩ
@@ -4632,6 +4632,36 @@ components:
description: The pool account's name
pinnedAt:
type: string
PoolGroup:
type: object
required:
- id
- name
- users
- accounts
properties:
id:
type: integer
name:
type: string
createdAt:
type: string
users:
type: array
items:
type: object
properties:
userId:
type: integer
username:
type: string
accounts:
type: object
description: Per tool (claude, codex, cursor), the pool account names it may use
additionalProperties:
type: array
items:
type: string
UserPrefs:
type: object
required:
@@ -15227,7 +15257,7 @@ paths:
tags:
- Auth
operationId: listPoolAccounts
summary: The Claude and Codex account pools as the balancer sees them (never a key or token)
summary: The Claude, Codex and Cursor account pools as the balancer sees them, and the pool groups (never a key or token)
parameters:
- name: force
in: query
@@ -15289,6 +15319,31 @@ paths:
type: array
items:
$ref: '#/components/schemas/AccountPin'
cursor:
type: array
items:
$ref: '#/components/schemas/AccountPin'
groups:
type: array
description: 'Pool groups (server/lib/pool-groups.js; empty in local mode): a member runs only on its group''s accounts, a user in no group on the accounts no group lists'
items:
$ref: '#/components/schemas/PoolGroup'
cursor:
type: array
description: 'The Cursor account pool (server/lib/cursor-pool.js): API keys only, no quota or spend'
items:
type: object
properties:
name:
type: string
kind:
type: string
enum:
- apikey
addedAt:
type: string
livePanes:
type: integer
codex:
type: array
description: The Codex account pool (server/lib/codex-pool.js), same fields plus `kind`; no expiry
@@ -15326,7 +15381,7 @@ paths:
tags:
- Auth
operationId: addPoolAccount
summary: 'Admin: add a Claude (saved login, pasted credentials or setup-token) or Codex (API key / auth.json text) account to the pool, with their own password again. The secret is never answered back'
summary: 'Admin: add a Claude (saved login, pasted credentials or setup-token), Codex (saved profile, API key / auth.json text) or Cursor (API key) account to the pool, with their own password again. The secret is never answered back'
requestBody:
required: true
content:
@@ -15342,6 +15397,7 @@ paths:
enum:
- claude
- codex
- cursor
name:
type: string
description: Defaults to the profile's name
@@ -15353,7 +15409,7 @@ paths:
description: 'Claude: the text of a .credentials.json'
token:
type: string
description: 'Claude: a setup-token; Codex: an API key or auth.json text'
description: 'Claude: a setup-token; Codex: an API key or auth.json text; Cursor: a Cursor API key'
currentPassword:
type: string
responses:
@@ -15411,6 +15467,7 @@ paths:
enum:
- claude
- codex
- cursor
userId:
type: integer
account:
@@ -15482,6 +15539,7 @@ paths:
enum:
- claude
- codex
- cursor
userIds:
type: array
items:
@@ -15515,6 +15573,172 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
/api/accounts/groups:
post:
tags:
- Auth
operationId: createPoolGroup
summary: 'Admin: a new, empty pool group, with their own password again'
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- name
- currentPassword
properties:
name:
type: string
currentPassword:
type: string
responses:
'201':
description: '`{group: {id, name}}`'
'400':
description: EBADNAME
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'403':
description: EREAUTH — wrong password; EADMIN — not an admin; ELOCALMODE
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'409':
description: EEXISTS — a group has that name
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'429':
description: ETOOMANY — too many wrong passwords
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
/api/accounts/groups/{id}:
patch:
tags:
- Auth
operationId: updatePoolGroup
summary: 'Admin: rename a pool group, set exactly its members (one group per user — a member of another group moves here) and/or exactly its accounts of each tool named, with their own password again'
parameters:
- name: id
in: path
required: true
schema:
type: integer
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- currentPassword
properties:
name:
type: string
userIds:
type: array
items:
type: integer
accounts:
type: object
description: '`{tool: [account names]}` for claude, codex, cursor'
additionalProperties:
type: array
items:
type: string
currentPassword:
type: string
responses:
'200':
description: '`{group}`'
content:
application/json:
schema:
type: object
properties:
group:
$ref: '#/components/schemas/PoolGroup'
'400':
description: EBADNAME, EBADUSER, EBADTOOL
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'403':
description: EREAUTH — wrong password; EADMIN — not an admin; ELOCALMODE
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'404':
description: ENOGROUP, ENOUSER, ENOACCOUNT
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'409':
description: EEXISTS — a group has that name
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'429':
description: ETOOMANY — too many wrong passwords
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
delete:
tags:
- Auth
operationId: deletePoolGroup
summary: 'Admin: delete a pool group — its members go back to the shared pool, no account is removed — with their own password again'
parameters:
- name: id
in: path
required: true
schema:
type: integer
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- currentPassword
properties:
currentPassword:
type: string
responses:
'200':
description: '`{ok: true}`'
'403':
description: EREAUTH — wrong password; EADMIN — not an admin; ELOCALMODE
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'404':
description: ENOGROUP
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'429':
description: ETOOMANY — too many wrong passwords
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
/api/accounts/{name}:
delete:
tags:
@@ -15542,6 +15766,7 @@ paths:
enum:
- claude
- codex
- cursor
currentPassword:
type: string
responses: