feat: per-tool settings, persistent settings file, Codex and Antigravity sessions, tokens and hooks (#40 B, C1, C2, D)

Part B: Model Pricing, Hook Configuration and CLAUDE_HOME move from
Settings → Dashboard to the Claude Code tab's AgentHub sub-tab
(ClaudeAgentHubSettings.tsx); CODEX_HOME is editable in Codex → AgentHub.
A home that is or contains the OS home is refused (EBADHOME).

Settings persistence: dashboard-saved settings go to
~/.config/agenthub/settings.env (AGENTHUB_SETTINGS_FILE), outside the plugin
cache an update wipes. Load order: real env > the checkout's .env > that file.
The first CLAUDE_HOME change pins DASHBOARD_DATA_DIR so the DB stays put.

C1: codex-usage.js reads Codex rollout journals into sessions (new column
tool) and token_usage, tracked per file in codex_rollouts; forks deduped by
total vector, sub-agent threads folded into the root; pricing-sync also
prices OpenAI models; PricingSettings with a vendor filter.

C2: codex-hooks.js installs AgentHub's hooks into $CODEX_HOME/hooks.json on
a click, through a stable forwarder; Codex trust is never written. The hook
route creates tool='codex' sessions and folds sub-agent hooks into the root.

D: antigravity-usage.js reads agy's per-conversation SQLite files (tokens
from ModelUsageStats) into tool='antigravity' sessions; antigravity-hooks.js
installs the named hook `agenthub` (PreInvocation, PostToolUse, Stop — never
PreToolUse) into ~/.gemini/config/hooks.json; Settings → Antigravity tab;
Gemini pricing.

Whole-change review fixes: sameOriginGuard on both home PUTs, routers.missing
on a CLAUDE_HOME change, reset-pricing scope, GET /api/sessions?tool=,
clear-data covering the new tables, Codex child-thread events moving no
status, hooks owning status only while current, EFOREIGN for a forwarder
that is not ours, agy quiet window and turn rules, AGENTHUB_SETTINGS_FILE
honoured from .env.

Docs, OpenAPI and the CLAUDE.md marker updated. Server 2141, client 779.
This commit is contained in:
2026-09-29 16:09:19 +07:00
parent 7ec5a7cd85
commit 969b06253c
95 changed files with 9278 additions and 1696 deletions
+42 -22
View File
@@ -20,28 +20,19 @@ for (const k of Object.keys(process.env)) {
// real editor setting is anything else and stays.
if (process.env.GIT_EDITOR === "true") delete process.env.GIT_EDITOR;
// Load .env file (simple key=value, no external dependency needed)
(function loadDotEnv() {
const fs = require("fs");
const os = require("os");
const envPath = require("path").resolve(__dirname, "..", ".env");
if (!fs.existsSync(envPath)) return;
for (const line of fs.readFileSync(envPath, "utf8").split("\n")) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#")) continue;
const eqIdx = trimmed.indexOf("=");
if (eqIdx === -1) continue;
const key = trimmed.slice(0, eqIdx).trim();
let val = trimmed.slice(eqIdx + 1).trim();
// Strip surrounding quotes (single or double)
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
val = val.slice(1, -1);
}
if (!process.env[key]) {
process.env[key] = val.replace(/^~(?=\/)/, os.homedir());
}
}
})();
// Load the repo `.env` (simple key=value), then persisted settings. The real
// environment always wins, then a checkout's own `.env`; a dashboard-saved
// value (CLAUDE_HOME, CODEX_HOME, the pinned DASHBOARD_DATA_DIR) lives in the
// settings file, outside the plugin cache an update replaces — see
// server/lib/settings-env.js. Only when this
// file IS the process: the test suite requires it dozens of times, and a
// developer's real settings file pins DASHBOARD_DATA_DIR at their live database.
if (require.main === module) {
const settingsEnv = require("./lib/settings-env");
settingsEnv.loadEnvFiles({
owned: [require("path").resolve(__dirname, "..", ".env")],
});
}
// Carry a pre-rebrand `~/.claude/agent-dashboard` over to `~/.claude/agenthub`.
//
@@ -513,6 +504,35 @@ function startBackgroundServices() {
} catch (err) {
console.warn("codex poll failed to start:", err.message);
}
// Codex sessions and token usage, from the same journals (codex-usage.js):
// a first pass over every rollout in the background, then the same beat.
try {
require("./lib/codex-usage").startCodexUsagePoll(broadcast);
} catch (err) {
console.warn("codex usage poll failed to start:", err.message);
}
// Antigravity CLI sessions and token usage, from agy's conversation files.
try {
const { broadcastLane } = require("./routes/lanes");
require("./lib/antigravity-usage").startAntigravityUsagePoll(broadcast, broadcastLane);
} catch (err) {
console.warn("antigravity usage poll failed to start:", err.message);
}
// Codex/Antigravity hooks the user installed forward through a script whose
// content names this build; point it here (the command in the tool's
// hooks.json, which Codex trusts, stays the same).
for (const [name, mod] of [
["codex-hooks", "./lib/codex-hooks"],
["antigravity-hooks", "./lib/antigravity-hooks"],
]) {
try {
const refreshed = require(mod).refreshScript();
if (refreshed === "written") console.log(`[${name}] forwarder updated to this build`);
if (refreshed === "failed") console.warn(`[${name}] could not update the forwarder`);
} catch (err) {
console.warn(`${name} forwarder refresh failed:`, err.message);
}
}
// A codex/antigravity pane that exits fires no hook, so release its lane on
// a beat instead (one `tmux has-session` per lane with a run). Off with
// DASHBOARD_RUN_HEAL_MS=0.