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:
2026-10-08 12:22:14 +07:00
parent 6ef9a5d4b7
commit a9c71cd905
133 changed files with 8007 additions and 1830 deletions
+67 -4
View File
@@ -40,7 +40,7 @@
const path = require("node:path");
const fs = require("node:fs");
const { spawn } = require("node:child_process");
const { spawn, spawnSync } = require("node:child_process");
// Resolve the repo root relative to this script's REAL location so the CLI
// works both from a checkout (./bin/agenthub.js) and through the global symlink
@@ -1397,14 +1397,36 @@ async function cmdReinstallHooks() {
* built client) and wait until /api/health answers. No-ops with a pointer to
* the live URL when a server is already up. The child is fully detached with
* its output appended to data/agenthub-server.log, so closing this terminal does
* not stop the dashboard; stop it later with `kill <pid>` (the PID is
* printed and registered in ~/.claude/.agenthub.json).
* not stop the dashboard; stop it later with `agenthub stop`.
*/
async function cmdStart(flags) {
// A Quit (`agenthub stop`) held the plugin bootstrap off; starting ends it.
require(path.join(REPO_ROOT, "server", "lib", "quit.js")).clearQuit();
if (await serverIsUp()) {
console.log(`${c.green("●")} Dashboard already running at ${c.bold(baseUrl())}`);
return;
}
// A plugin install starts the way a Claude Code session does: its bootstrap
// knows the runtime dir, the UI bundle and the autostart unit.
if (/[\\/]\.claude[\\/]plugins[\\/]cache[\\/]/.test(REPO_ROOT)) {
spawnSync(
process.execPath,
[path.join(REPO_ROOT, "scripts", "plugin-bootstrap.js"), "--worker"],
{
stdio: "inherit",
}
);
const deadline = Date.now() + 30_000;
while (Date.now() < deadline) {
if (await serverIsUp()) {
console.log(`${c.green("●")} Dashboard ${c.bold("up")} at ${c.bold(baseUrl())}`);
return;
}
await new Promise((r) => setTimeout(r, 500));
}
console.error(c.red("✖ Server did not become healthy within 30 s — run /agenthub-doctor"));
process.exit(1);
}
const clientDist = path.join(REPO_ROOT, "client", "dist", "index.html");
if (!fs.existsSync(clientDist)) {
console.error(c.red("✖ client/dist is missing — the production server needs a built client."));
@@ -1447,7 +1469,7 @@ async function cmdStart(flags) {
console.log(
`${c.green("●")} Dashboard ${c.bold("up")} at ${c.bold(baseUrl())} ${c.dim(`(pid ${child.pid})`)}`
);
console.log(c.dim(` Stop it with: kill ${child.pid}`));
console.log(c.dim(" Stop it with: agenthub stop"));
return;
}
tick++;
@@ -1463,6 +1485,33 @@ async function cmdStart(flags) {
process.exit(1);
}
/**
* Quit AgentHub (issue #65): every lane pane is killed, the server stops, and
* the plugin bootstrap starts none until `agenthub start` — or a boot/login
* through an enabled autostart unit (`server/lib/quit.js`).
*/
async function cmdStop() {
if (!(await serverIsUp())) {
require(path.join(REPO_ROOT, "server", "lib", "quit.js")).markQuit();
console.log(
`${c.dim("○")} Dashboard is not running — it stays stopped until ${c.bold("agenthub start")}`
);
return;
}
const { killed } = await post("/api/settings/quit");
const deadline = Date.now() + 15_000;
while ((await serverIsUp()) && Date.now() < deadline) {
await new Promise((r) => setTimeout(r, 250));
}
if (await serverIsUp()) {
console.error(c.red(`✖ The dashboard at ${baseUrl()} is still answering after 15 s`));
process.exit(1);
}
console.log(
`${c.green("✔")} AgentHub stopped${killed ? ` — ${killed} terminal session(s) closed` : ""}. Start it again with: ${c.bold("agenthub start")}`
);
}
/**
* Installs/removes a systemd `--user` service (Linux) or a LaunchAgent
* (macOS) so the dashboard survives reboots without anything (Claude Code
@@ -2731,6 +2780,11 @@ const COMMAND_GROUPS = [
[
["status", "", "Up/down indicator for the dashboard server"],
["start", "[--port N]", "Start the server in the background and wait for healthy"],
[
"stop",
"",
"Quit: close every workspace terminal and stop the server until `agenthub start`",
],
[
"service",
"install|uninstall|status [--port N]",
@@ -2948,6 +3002,11 @@ const COMMAND_GROUPS = [
"",
"Run the read-only MCP server on stdio (claude mcp add agenthub -- agenthub mcp)",
],
[
"tray",
"",
"Show AgentHub's icon in the OS status area now (the server keeps it up; Settings → System hides it)",
],
["statusline", "", "Render the Claude Code statusline from the JSON on stdin"],
[
"statusline install|uninstall",
@@ -4000,6 +4059,8 @@ async function runCommand(argv) {
return cmdStatus();
case "start":
return cmdStart(flags);
case "stop":
return cmdStop();
case "service":
return cmdService(positional[0], flags);
case "server":
@@ -4135,6 +4196,8 @@ async function runCommand(argv) {
return;
}
return require(path.join(REPO_ROOT, "statusline", "statusline.js")).main();
case "tray":
return require(path.join(REPO_ROOT, "scripts", "tray.js")).main();
case "open":
return cmdOpen();
case "version":