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
+41 -8
View File
@@ -2855,8 +2855,8 @@ const COMMAND_GROUPS = [
],
[
"accounts",
"list|add|remove [name]|pin <account> <user>|unpin <user> [--tool codex]",
"Claude/Codex account pool: new sessions spread over these accounts; pin = one user always on one",
"list|add|remove [name]|pin <account> <user>|unpin <user> [--tool codex|cursor]",
"Claude/Codex/Cursor account pool: new sessions spread over these accounts; pin = one user always on one",
],
["repl", "", "Open the interactive shell (also: shell, i)"],
],
@@ -4328,8 +4328,36 @@ async function cmdCodexAccounts(sub, name, flags) {
);
}
/** `agenthub accounts … --tool cursor` — the Cursor account pool (server/lib/cursor-pool.js). */
async function cmdCursorAccounts(sub, name) {
const pool = require(path.join(REPO_ROOT, "server", "lib", "cursor-pool.js"));
if (sub === "add") {
console.log(
c.dim(" Create an API key in the Cursor dashboard (Integrations), then paste it.")
);
const added = pool.addAccount(name, {
apiKey: await askPassword(`Cursor API key for ${name}: `),
});
console.log(`${c.green("✔")} ${added.name} added`);
console.log(c.dim(" New Cursor sessions now spread over the pool's accounts."));
return;
}
if (sub === "remove") {
pool.removeAccount(name);
console.log(`${c.green("✔")} ${name} removed — running panes keep it until they exit`);
return;
}
if (sub !== undefined && sub !== "list") {
throw new Error("usage: agenthub accounts list | add <name> | remove <name> --tool cursor");
}
table(
["cursor account", "live", "pinned"],
pool.poolStatus().map((a) => [a.name, a.livePanes, pinnedUsers("cursor", a.name)])
);
}
/**
* `agenthub accounts pin <account> <user>` / `unpin <user>` [--tool codex] —
* `agenthub accounts pin <account> <user>` / `unpin <user>` [--tool codex|cursor] —
* that user's new sessions run on that account (server/lib/account-pins.js).
*/
async function cmdAccountPin(sub, positional, tool) {
@@ -4337,7 +4365,9 @@ async function cmdAccountPin(sub, positional, tool) {
const [first, second] = positional;
const username = sub === "pin" ? second : first;
if (!username || (sub === "pin" && !first)) {
throw new Error("usage: agenthub accounts pin <account> <user> | unpin <user> [--tool codex]");
throw new Error(
"usage: agenthub accounts pin <account> <user> | unpin <user> [--tool codex|cursor]"
);
}
const user = require(path.join(REPO_ROOT, "server", "db.js"))
.db.prepare("SELECT id FROM users WHERE username = ?")
@@ -4387,14 +4417,15 @@ async function cmdAccounts(sub, positional, flags = {}) {
const pool = require(path.join(REPO_ROOT, "server", "lib", "claude-pool.js"));
const name = positional[0];
if (sub === "add" || sub === "remove") {
if (!name) throw new Error(`usage: agenthub accounts ${sub} <name> [--tool codex]`);
if (!name) throw new Error(`usage: agenthub accounts ${sub} <name> [--tool codex|cursor]`);
}
if (flags.tool != null && flags.tool !== "claude" && flags.tool !== "codex") {
throw new Error("--tool is claude (the default) or codex");
if (flags.tool != null && !["claude", "codex", "cursor"].includes(flags.tool)) {
throw new Error("--tool is claude (the default), codex or cursor");
}
if (sub === "pin" || sub === "unpin")
return cmdAccountPin(sub, positional, flags.tool || "claude");
if (flags.tool === "codex") return cmdCodexAccounts(sub, name, flags);
if (flags.tool === "cursor") return cmdCursorAccounts(sub, name);
if (sub === "add") {
console.log(
c.dim(" Run `claude setup-token` signed in as that account, then paste the token.")
@@ -4450,11 +4481,13 @@ async function cmdAccounts(sub, positional, flags = {}) {
);
const codex = require(path.join(REPO_ROOT, "server", "lib", "codex-pool.js"));
if (codex.poolStatus().length) await cmdCodexAccounts("list", null, flags);
const cursor = require(path.join(REPO_ROOT, "server", "lib", "cursor-pool.js"));
if (cursor.poolStatus().length) await cmdCursorAccounts("list", null);
return;
}
console.error(
c.red(
"✖ Usage: agenthub accounts list | add <name> | remove <name> | pin <account> <user> | unpin <user> [--tool codex]"
"✖ Usage: agenthub accounts list | add <name> | remove <name> | pin <account> <user> | unpin <user> [--tool codex|cursor]"
)
);
process.exitCode = 1;